From 84bae6918badd40fac4bdc2957516a05e34a2ed0 Mon Sep 17 00:00:00 2001 From: tae2089 Date: Sat, 11 Jul 2026 11:39:04 +0900 Subject: [PATCH] Remove the automatic postprocess failure-policy machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Postprocess steps (stored-flow + search-index rebuild) run on every build/update; the escalation layer on top of them — auto fail_closed after repeated failures, per-namespace run-log history, and two inspection tools — was operational overhead the project's purpose does not need. Step failures are already visible: they warn to the log and list in the `failed_steps` response field with `status: "degraded"`. - delete internal/postprocess/policy and its two GORM models - add migration 000007 dropping ccg_postprocess_policy_state and ccg_postprocess_run_logs (sqlite + postgres); RequiredSchemaVersion 6 -> 7 - remove get_postprocess_policy and reset_postprocess_policy MCP tools (26 -> 24) - drop the postprocess_policy request arg and fail_closed path; keep the inline degraded behavior (warn + failed_steps) in build_or_update_graph and run_postprocess - remove the PostprocessPolicy Deps field, runtime wiring, HTTP /status postprocess summary, and the `ccg status` postprocess section (+ --errors/--recent flags) - delete guide/postprocess-failure-policy.md (EN + ko) and sweep doc references Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 5 +- cmd/ccg/health_test.go | 54 +-- cmd/ccg/main_test.go | 36 +- guide/README.md | 1 - guide/cli-reference.md | 4 +- guide/ko/README.md | 1 - guide/ko/cli-reference.md | 4 +- guide/ko/lint.md | 1 - guide/ko/mcp-tools.md | 12 +- guide/ko/operations.md | 6 +- guide/ko/postprocess-failure-policy.md | 174 -------- guide/ko/webhook.md | 2 +- guide/lint.md | 1 - guide/mcp-tools.md | 23 +- guide/operations.md | 6 +- guide/postprocess-failure-policy.md | 190 -------- guide/webhook.md | 2 +- internal/cli/status.go | 66 --- internal/cli/status_test.go | 169 ------- internal/db/migration/migration.go | 36 +- internal/mcp/deps.go | 19 +- internal/mcp/handler_parse.go | 196 ++------- internal/mcp/handler_postprocess.go | 74 ---- internal/mcp/handlers_test.go | 130 ------ internal/mcp/server_test.go | 8 +- internal/mcp/testhelpers_test.go | 95 +--- internal/mcp/tools_parse.go | 4 +- internal/mcp/tools_postprocess.go | 29 -- internal/mcp/tools_register.go | 1 - internal/mcpruntime/runtime.go | 63 +-- .../000007_drop_postprocess_policy.down.sql | 22 + .../000007_drop_postprocess_policy.up.sql | 3 + .../000007_drop_postprocess_policy.down.sql | 22 + .../000007_drop_postprocess_policy.up.sql | 3 + internal/model/postprocess_policy.go | 39 -- internal/postprocess/policy/engine.go | 411 ------------------ internal/postprocess/policy/engine_test.go | 372 ---------------- internal/server/serve.go | 34 +- internal/store/gormstore/gormstore.go | 2 - internal/store/gormstore/gormstore_test.go | 29 -- 40 files changed, 149 insertions(+), 2200 deletions(-) delete mode 100644 guide/ko/postprocess-failure-policy.md delete mode 100644 guide/postprocess-failure-policy.md delete mode 100644 internal/mcp/handler_postprocess.go delete mode 100644 internal/mcp/tools_postprocess.go create mode 100644 internal/migrationfs/postgres/000007_drop_postprocess_policy.down.sql create mode 100644 internal/migrationfs/postgres/000007_drop_postprocess_policy.up.sql create mode 100644 internal/migrationfs/sqlite/000007_drop_postprocess_policy.down.sql create mode 100644 internal/migrationfs/sqlite/000007_drop_postprocess_policy.up.sql delete mode 100644 internal/model/postprocess_policy.go delete mode 100644 internal/postprocess/policy/engine.go delete mode 100644 internal/postprocess/policy/engine_test.go diff --git a/CLAUDE.md b/CLAUDE.md index e22713c..aeadf79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,9 @@ Follow the global prompt rules first. This file adds project-specific skill rout ## MCP 서버 -`.mcp.json`에 등록된 ccg MCP 서버가 26개 도구를 제공합니다: +`.mcp.json`에 등록된 ccg MCP 서버가 24개 도구를 제공합니다: - `parse_project`, `build_or_update_graph`, `run_postprocess` -- `get_postprocess_policy`, `reset_postprocess_policy` - `get_node`, `search`, `query_graph`, `list_graph_stats`, `get_minimal_context` - `get_impact_radius`, `trace_flow`, `find_suspect_fallback_edges` - `detect_changes`, `get_affected_flows`, `list_flows` @@ -49,7 +48,7 @@ Graceful shutdown: SIGINT/SIGTERM 시 진행 중인 clone/build에 context cance 상세 문서는 `guide/` 디렉토리를 참조하세요: - [CLI Reference](guide/cli-reference.md) — 전체 명령어, 플래그, 설정 파일 -- [MCP Tools](guide/mcp-tools.md) — 26개 MCP 도구, Skills, AI-Driven Annotation +- [MCP Tools](guide/mcp-tools.md) — 24개 MCP 도구, Skills, AI-Driven Annotation - [Annotations](guide/annotations.md) — 어노테이션 태그, 예시, 검색 - [Webhook](guide/webhook.md) — Webhook sync, 브랜치 필터링, HMAC, graceful shutdown - [Docker](guide/docker.md) — Docker 빌드, MCP 서버, PostgreSQL 배포 diff --git a/cmd/ccg/health_test.go b/cmd/ccg/health_test.go index 79e1c9c..4e03ff2 100644 --- a/cmd/ccg/health_test.go +++ b/cmd/ccg/health_test.go @@ -10,9 +10,8 @@ import ( "testing" "time" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" - ccgserver "github.com/tae2089/code-context-graph/internal/server" ccgdb "github.com/tae2089/code-context-graph/internal/db" + ccgserver "github.com/tae2089/code-context-graph/internal/server" "github.com/tae2089/code-context-graph/internal/webhook" ) @@ -113,7 +112,7 @@ func TestStatusHandler_ReportsWebhookQueueFull(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String()) @@ -136,7 +135,7 @@ func TestStatusHandler_ReportsWebhookQueueAges(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) @@ -207,7 +206,7 @@ func TestStatusHandler_ReportsUnresolvedWebhookFailure(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) @@ -283,7 +282,7 @@ func TestStatusHandler_DegradedWhenRecentRepoFailureUnresolved(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) @@ -326,7 +325,7 @@ func TestStatusHandler_DBNotReadyTakesPrecedenceOverWebhookDegraded(t *testing.T req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return errors.New("db down") }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return errors.New("db down") }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503, got %d body=%s", rec.Code, rec.Body.String()) @@ -383,7 +382,7 @@ func TestStatusHandler_ReportsPerRepoRecentRepos(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) @@ -444,7 +443,7 @@ func TestStatusHandler_RecentRepos_FailureHasErrorFields(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { @@ -476,7 +475,7 @@ func TestStatusHandler_ExistingAggregateFieldsPreserved(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/status", nil) rec := httptest.NewRecorder() - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }, nil).ServeHTTP(rec, req) + ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, func() *webhook.SyncQueue { return q }).ServeHTTP(rec, req) var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { @@ -493,41 +492,6 @@ func TestStatusHandler_ExistingAggregateFieldsPreserved(t *testing.T) { } } -func TestStatusHandler_ReportsPostprocessSummary(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/status", nil) - rec := httptest.NewRecorder() - - ccgserver.StatusHandler(func(r *http.Request) error { return nil }, time.Minute, nil, func(context.Context) (*postprocesspolicy.StatusSummary, error) { - return &postprocesspolicy.StatusSummary{ - Status: postprocesspolicy.StatusDegraded, - FailClosed: []postprocesspolicy.StateSnapshot{{ - Namespace: "default", - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocesspolicy.PolicyFailClosed, - ConsecutiveFailures: 3, - }}, - }, nil - }).ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) - } - var body map[string]any - if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { - t.Fatalf("decode status: %v", err) - } - if got := body["status"]; got != "degraded" { - t.Fatalf("status = %v, want degraded", got) - } - postprocessBody, ok := body["postprocess"].(map[string]any) - if !ok { - t.Fatalf("missing postprocess body: %v", body) - } - if got := postprocessBody["status"]; got != "degraded" { - t.Fatalf("postprocess.status = %v, want degraded", got) - } -} - type recordingPool struct { maxOpen int maxIdle int diff --git a/cmd/ccg/main_test.go b/cmd/ccg/main_test.go index 423c839..c0d701a 100644 --- a/cmd/ccg/main_test.go +++ b/cmd/ccg/main_test.go @@ -109,7 +109,7 @@ func TestRunMigrations_AllowsRuntimeSchemaCheck(t *testing.T) { } } -func TestRunMigrations_SqliteAppliesPolicyTables(t *testing.T) { +func TestRunMigrations_SqliteDropsPolicyTablesAtHead(t *testing.T) { requireSQLiteFTS5(t) dbPath := filepath.Join(t.TempDir(), "ccg.db") @@ -128,29 +128,11 @@ func TestRunMigrations_SqliteAppliesPolicyTables(t *testing.T) { t.Fatalf("run migrations: %v", err) } - if !db.Migrator().HasTable("ccg_postprocess_policy_state") { - t.Fatal("expected ccg_postprocess_policy_state after migrations") - } - if !db.Migrator().HasTable("ccg_postprocess_run_logs") { - t.Fatal("expected ccg_postprocess_run_logs after migrations") - } - - var version migration.MigrationSchemaVersion - if err := db.Table("schema_migrations").First(&version).Error; err != nil { - t.Fatalf("load schema version: %v", err) - } - if int(version.Version) != migration.RequiredSchemaVersion { - t.Fatalf("schema version = %d, want %d", version.Version, migration.RequiredSchemaVersion) - } - if version.Dirty { - t.Fatal("schema version is dirty") - } - - if !db.Migrator().HasColumn("ccg_postprocess_policy_state", "namespace") { - t.Fatal("expected namespace column on ccg_postprocess_policy_state") + if db.Migrator().HasTable("ccg_postprocess_policy_state") { + t.Fatal("expected ccg_postprocess_policy_state to be dropped at head schema") } - if !db.Migrator().HasColumn("ccg_postprocess_policy_state", "tool") { - t.Fatal("expected tool column on ccg_postprocess_policy_state") + if db.Migrator().HasTable("ccg_postprocess_run_logs") { + t.Fatal("expected ccg_postprocess_run_logs to be dropped at head schema") } } @@ -268,7 +250,7 @@ func TestRunMigrations_SQLiteDownRestoresNullableColumns(t *testing.T) { if err != nil { t.Fatalf("create migrator: %v", err) } - if err := migrator.Steps(-5); err != nil { + if err := migrator.Steps(-6); err != nil { t.Fatalf("run down migration: %v", err) } @@ -313,7 +295,7 @@ func TestRunMigrations_SQLiteDownFromVersionThreeDropsPolicyTables(t *testing.T) if err != nil { t.Fatalf("create migrator: %v", err) } - if err := migrator.Steps(-4); err != nil { + if err := migrator.Steps(-5); err != nil { t.Fatalf("run down migration: %v", err) } @@ -427,7 +409,7 @@ func TestEnsureSchemaVersion_LogsRuntimeSchemaPassAndFail(t *testing.T) { t.Fatalf("ensure schema version: %v", err) } passLog := logs.String() - for _, want := range []string{"database runtime schema check passed", "driver=sqlite", "required_version=6", "auto_migrated=true"} { + for _, want := range []string{"database runtime schema check passed", "driver=sqlite", "required_version=7", "auto_migrated=true"} { if !strings.Contains(passLog, want) { t.Fatalf("expected runtime schema pass log to contain %q, got %q", want, passLog) } @@ -441,7 +423,7 @@ func TestEnsureSchemaVersion_LogsRuntimeSchemaPassAndFail(t *testing.T) { t.Fatal("expected parity failure") } failLog := logs.String() - for _, want := range []string{"database runtime schema check failed", "driver=sqlite", "required_version=6", "auto_migrated=false", "error.message="} { + for _, want := range []string{"database runtime schema check failed", "driver=sqlite", "required_version=7", "auto_migrated=false", "error.message="} { if !strings.Contains(failLog, want) { t.Fatalf("expected runtime schema failure log to contain %q, got %q", want, failLog) } diff --git a/guide/README.md b/guide/README.md index d402392..2dd6c2a 100644 --- a/guide/README.md +++ b/guide/README.md @@ -25,7 +25,6 @@ explore graph edges. | [Webhook](webhook.md) | GitHub / Gitea webhook sync, branch filtering, graceful shutdown | | [Docker](docker.md) | Docker image build, MCP server setup, Wiki UI deployment, PostgreSQL integration | | [Operations](operations.md) | Deployment profiles, database choice, readiness, webhook operations, troubleshooting | -| [Postprocess Failure Policy](postprocess-failure-policy.md) | Status rules, failure causes, and automatic degraded/fail_closed policy for build and postprocess tools | | [Runtime Layout](runtime-layout.md) | `ccg`, `ccg-server`, Wiki serving, and shared `ccg-core` ownership boundaries | | [Architecture](architecture.md) | System architecture, data flow, DB schema | | [Development](development.md) | Build, test, integration test (Gitea + PostgreSQL) | diff --git a/guide/cli-reference.md b/guide/cli-reference.md index 110bcae..74d4c25 100644 --- a/guide/cli-reference.md +++ b/guide/cli-reference.md @@ -41,9 +41,7 @@ ccg update ./backend --namespace backend | `ccg build --fallback-calls` | Enable best-effort fallback call resolution (strict by default) | | `ccg update [dir]` | Incremental sync | | `ccg update --fallback-calls` | Enable best-effort fallback call resolution for incremental sync | -| `ccg status` | Graph statistics and postprocess error summary | -| `ccg status --errors` | Include recent postprocess failure details | -| `ccg status --recent ` | Number of recent postprocess failures to inspect (default `5`) | +| `ccg status` | Graph statistics | | `ccg search ` | Full-text search | | `ccg search --path ` | Scoped search by path prefix | | `ccg docs [--out dir]` | Generate Markdown documentation, the `wiki-index.json` compatibility snapshot, and the default RAG index (prunes stale generator-managed docs by default) | diff --git a/guide/ko/README.md b/guide/ko/README.md index 8239ef4..2d96659 100644 --- a/guide/ko/README.md +++ b/guide/ko/README.md @@ -29,7 +29,6 @@ matched fields, evidence node로 빠르게 경로를 좁힌 뒤 graph/search 도 | [웹훅(Webhook)](webhook.md) | GitHub / Gitea 웹훅 동기화, 브랜치 필터링, Graceful Shutdown | | [Docker](docker.md) | Docker 이미지 빌드, MCP 서버 설정, Wiki UI 배포, PostgreSQL 연동 | | [운영(Operations)](operations.md) | 배포 프로필, 데이터베이스 선택, 준비성 체크, 웹훅 운영 및 문제 해결 | -| [사후 처리 실패 정책](postprocess-failure-policy.md) | 상태 규칙, 실패 원인, 빌드 및 사후 처리 도구의 자동 성능 저하(degraded)/폐쇄형 실패(fail_closed) 정책 | | [런타임 구조](runtime-layout.md) | `ccg`, `ccg-server`, Wiki serving, 공용 `ccg-core` 소유권 경계 | | [아키텍처](architecture.md) | 시스템 아키텍처, 데이터 흐름, DB 스키마 | | [개발(Development)](development.md) | 빌드, 테스트, 통합 테스트(Gitea + PostgreSQL) | diff --git a/guide/ko/cli-reference.md b/guide/ko/cli-reference.md index 9dd14a2..7472174 100644 --- a/guide/ko/cli-reference.md +++ b/guide/ko/cli-reference.md @@ -43,9 +43,7 @@ ccg update ./backend --namespace backend | `ccg build --fallback-calls` | (기본 off) best-effort fallback 호출 해상도 활성화 | | `ccg update [dir]` | 증분 동기화(Incremental sync) | | `ccg update --fallback-calls` | 증분 동기화에서 best-effort fallback 호출 해상도 활성화 | -| `ccg status` | 그래프 통계 및 사후 처리 오류 요약 출력 | -| `ccg status --errors` | 최근 사후 처리 실패 상세 포함 | -| `ccg status --recent ` | 확인할 최근 사후 처리 실패 개수 (기본값 `5`) | +| `ccg status` | 그래프 통계 출력 | | `ccg search ` | 전체 텍스트 검색 | | `ccg search --path ` | 경로 접두사로 검색 범위 제한 | | `ccg docs [--out dir]` | 마크다운 문서, `wiki-index.json` 호환 snapshot, 기본 RAG 인덱스 생성 (기본적으로 그래프에 없는 generator-managed 문서를 prune) | diff --git a/guide/ko/lint.md b/guide/ko/lint.md index 608dcb8..5864f2a 100644 --- a/guide/ko/lint.md +++ b/guide/ko/lint.md @@ -267,4 +267,3 @@ rules: - [CLI 레퍼런스](cli-reference.md#lint-categories) - [커스텀 어노테이션](annotations.md) - [개발](development.md) -- [사후 처리 실패 정책](postprocess-failure-policy.md) diff --git a/guide/ko/mcp-tools.md b/guide/ko/mcp-tools.md index 6f5590b..5b72f4e 100644 --- a/guide/ko/mcp-tools.md +++ b/guide/ko/mcp-tools.md @@ -41,20 +41,14 @@ Codex 또는 Claude Code 같은 MCP 지원 코딩 에이전트가 연결할 수 |------|-------------| | `parse_project` | 소스 파일 파싱 | | `build_or_update_graph` | 사후 처리를 포함한 전체/증분 빌드 | -| `run_postprocess` | 저장된 흐름(flow), 커뮤니티 및 전체 텍스트 검색 파생 상태 재생성 | -| `get_postprocess_policy` | 자동 사후 처리 정책 상태 및 최근 실패 내역 확인 | -| `reset_postprocess_policy` | 특정 도구에 대한 fail-closed 연속 기록을 지우기 위한 리셋 마커 기록 | +| `run_postprocess` | 저장된 흐름(flow) 및 전체 텍스트 검색 파생 상태 재생성 | | `get_node` | 정규화된 이름으로 노드 조회 | | `search` | 전체 텍스트 검색 | | `query_graph` | 정의된 그래프 쿼리(callers, callees, imports 등) | | `list_graph_stats` | 노드/엣지/파일 수 확인 | -| `get_minimal_context` | AI 에이전트 진입점을 위한 경량 요약(~100 토큰) — 그래프 통계, 리스크, 주요 커뮤니티/흐름, 도구 추천 포함 | +| `get_minimal_context` | AI 에이전트 진입점을 위한 경량 요약(~100 토큰) — 그래프 통계, 리스크, 주요 흐름, 도구 추천 포함 | -`build_or_update_graph`와 `run_postprocess`는 모두 자동 사후 처리 실패 정책(postprocess failure policy)을 지원합니다. 명시적인 `postprocess_policy`가 제공되지 않으면 CCG는 기본적으로 `degraded` 모드를 사용하며, 동일한 `(namespace, tool)` 쌍에 대해 3회 연속 `degraded` 실행 시 자동으로 `fail_closed`로 격상합니다. - -상태 표, 실패 원인, 건너뛰기 동작 및 `build_or_update_graph`와 `run_postprocess`에 대한 정책 격상 규칙 등 자세한 내용은 [사후 처리 실패 정책](postprocess-failure-policy.md)을 참조하십시오. - -CCG는 아직 Prometheus `/metrics` 엔드포인트를 제공하지 않습니다. 사후 처리 작업에 대해서는 현재 기계가 읽을 수 있는 운영 인터페이스인 `get_postprocess_policy`와 HTTP `/status` 요약을 사용하십시오. +`build_or_update_graph`와 `run_postprocess`는 그래프 기록 후 파생 상태 단계(저장된 흐름, 전체 텍스트 검색)를 실행합니다. 단계가 실패하면 도구는 여전히 `status: "degraded"`와 실패한 단계를 담은 `failed_steps`가 포함된 구조화된 결과를 반환하며, 실패는 로그에도 남습니다. 이 응답(또는 HTTP `/status` 요약)으로 성능 저하된 사후 처리를 감지하십시오. ### 분석 (Analysis) diff --git a/guide/ko/operations.md b/guide/ko/operations.md index f86012b..171bd5a 100644 --- a/guide/ko/operations.md +++ b/guide/ko/operations.md @@ -198,7 +198,7 @@ SQLite 웹훅 배포는 `--webhook-workers` 또는 `CCG_WEBHOOK_WORKERS`가 명 | 신호 | 의미 | 운영자 조치 | |--------|---------|-----------------| | `/ready`가 `not_ready` 반환 | DB 사용 불가, 큐 가득 참, 또는 차단 중인 큐 대기 시간 | 트래픽 전송을 중단하고 로그/상태를 조사하십시오. | -| `/status`가 `degraded` | 마지막 웹훅 또는 사후 처리 상태 확인 필요 | 실패한 저장소/설정을 수정하고 새로운 push 또는 수동 업데이트로 재시도하십시오. | +| `/status`가 `degraded` | 마지막 웹훅 상태 확인 필요 | 실패한 저장소/설정을 수정하고 새로운 push 또는 수동 업데이트로 재시도하십시오. | | 큐 대기 시간이 꾸준히 증가함 | 워커가 유입되는 push를 감당하지 못함 | 저장소 범위를 줄이거나, PostgreSQL에서 워커를 늘리거나, 네임스페이스를 분리하십시오. | | 검색 결과가 오래된 것 같음 | 검색 사후 처리가 실패했거나 건너뛰어졌을 수 있음 | `fts=true`로 `run_postprocess`를 실행하거나 네임스페이스를 재빌드/업데이트하십시오. | @@ -229,7 +229,7 @@ SIGINT/SIGTERM 수신 시: ```bash ccg update /data/repos/api --namespace api -ccg status --namespace api --errors +ccg status --namespace api ``` 검색, 커뮤니티, 저장된 flow가 여전히 오래된 것처럼 보이면 namespace `api`에 대해 필요한 postprocess 플래그와 함께 MCP `run_postprocess` 도구를 호출하십시오. @@ -261,6 +261,6 @@ CCG를 업그레이드한 후, 기존의 기본 `ccg.db` 또한 기존 스키마 | 웹훅이 금지됨(forbidden) 반환 | 허용되지 않은 저장소 또는 브랜치 | `--allow-repo` 패턴 및 브랜치 ref를 확인하십시오. | | 웹훅이 너무 많은 요청 반환 | 동기화 큐가 가득 참 | `/status`를 확인하고, push 볼륨을 줄이거나 PostgreSQL에서 워커를 늘리십시오. | | `/ready`가 `not_ready`임 | DB 또는 큐 차단 조건 | `/status` 및 서비스 로그를 조사하십시오. | -| `/status`가 `degraded`임 | 마지막 웹훅 또는 사후 처리 실패 | 저장소 설정을 수정하거나 업데이트/사후 처리를 재실행하십시오. | +| `/status`가 `degraded`임 | 마지막 웹훅 실패 | 저장소 설정을 수정하거나 업데이트/사후 처리를 재실행하십시오. | | 검색에서 최근 코드가 누락됨 | FTS/검색 문서가 오래됨 | `fts=true`로 `run_postprocess`를 실행하거나 네임스페이스를 재빌드하십시오. | | 시작 시 마이그레이션 오류 | 스키마 버전 불일치, 마이그레이션 소스 불일치, 또는 스키마 drift | 배포된 바이너리 버전에서 `ccg migrate`를 실행하십시오. 여전히 실패하면 설정된 마이그레이션 소스와 스키마 drift를 확인하십시오. | diff --git a/guide/ko/postprocess-failure-policy.md b/guide/ko/postprocess-failure-policy.md deleted file mode 100644 index 99242d9..0000000 --- a/guide/ko/postprocess-failure-policy.md +++ /dev/null @@ -1,174 +0,0 @@ -# 사후 처리 실패 정책 (Postprocess Failure Policy) - -[English](../postprocess-failure-policy.md) - -이 가이드는 파생된 그래프 상태를 재생성할 수 있는 두 가지 MCP 도구에서 `ok`, `degraded`, `fail_closed`, `skipped_steps`가 어떻게 보고되는지 설명합니다: - -- `build_or_update_graph` -- `run_postprocess` - -또한 호출자가 명시적인 `postprocess_policy`를 제공하지 않았을 때 `degraded` 또는 `fail_closed`를 선택하는 자동 정책 엔진에 대해서도 설명합니다. - -## 용어 정의 (Terms) - -| 용어 | 의미 | -|------|---------| -| `ok` | 요청된 작업이 성공했거나 건너뛴 단계만 남은 상태입니다. | -| `degraded` | 요청된 일부 사후 처리 단계가 실패했지만, 도구가 구조화된 성공 결과를 반환한 상태입니다. | -| `fail_closed` | 사후 처리 단계의 오류를 허용하지 않는 실패 정책입니다. 요청된 단계가 실패하면 도구는 해당 호출에 대해 에러를 반환합니다. | -| `skipped_steps` | 호출자가 비활성화했거나, 선택된 모드에 포함되지 않았거나, 필요한 빌더/백엔드가 설정되지 않아 시도되지 않은 단계들입니다. | - -## 자동 정책 엔진 (Automatic policy engine) - -호출자가 `postprocess_policy`를 생략하면 CCG는 다음 순서에 따라 유효 정책을 결정합니다: - -1. 호출자가 값을 명시적으로 제공한 경우 해당 값이 우선합니다. -2. 제공되지 않은 경우, 자동 정책 엔진은 동일한 `(namespace, tool)` 쌍에 대한 최근 실행 기록을 확인합니다. -3. 기본 정책은 `degraded`입니다. -4. 동일한 `(namespace, tool)`에 대해 **3회 연속 `degraded` 실행**이 발생하면, 자동 정책은 `fail_closed`로 격상됩니다. -5. 이후 `ok` 실행이 발생하면 연속 실패 기록이 초기화됩니다. - -정책 상태는 다음과 같이 저장됩니다: - -- `ccg_postprocess_policy_state`: 현재 유효 상태 -- `ccg_postprocess_run_logs`: 추가 전용 실행 이력 - -운영자를 위해 CCG는 두 가지 경량 제어 인터페이스를 제공합니다: - -- `get_postprocess_policy`: 현재 fail-closed 항목 및 최근 실패 내역을 반환합니다. -- `reset_postprocess_policy`: 현재 네임스페이스의 특정 도구에 대해 리셋 마커 실행을 기록합니다. - -리셋 경로는 실행 이력을 삭제하지 않습니다. 대신 소스가 `reset`인 `ok` 마커를 삽입하여, 자동 결정 엔진이 사용하는 연속 실패 기록을 안전하게 끊어줍니다. - -실행 로그는 쓰기 작업 후 기회적으로(opportunistically) 정리되어, 각 `(namespace, tool)`이 영원히 늘어나는 대신 제한된 최신 이력만 유지하도록 관리됩니다. - -## 네임스페이스 격리 및 공유 상태 - -자동 정책 결정 및 사후 처리 결과의 범위는 현재 `namespace`로 제한됩니다. - -- 정책 격상은 `(namespace, tool)`별로 추적됩니다. -- 파생 상태 재생성(`flows`, `communities`, `search_documents`, `fts`)은 활성 네임스페이스에만 적용됩니다. -- 한 네임스페이스에서의 실패가 다른 네임스페이스를 직접 `degraded` 또는 `fail_closed`로 표시하지 않습니다. - -정상적인 운영 상황에서 이는 네임스페이스 `A`에서 재생성이 실패하여 해당 네임스페이스만 오래된 상태가 되더라도, 네임스페이스 `B`는 정상적으로 쿼리를 수행할 수 있음을 의미합니다. - -주요 예외는 스키마 마이그레이션 호환성과 같은 진정한 전역 운영 상태입니다. 예를 들어 `SchemaVersion`은 네임스페이스 범위가 아니므로, 사후 처리 정책과 파생 그래프 상태가 네임스페이스별로 격리되어 있더라도 전역 스키마 불일치는 전체 배포에 영향을 줄 수 있습니다. - -## `build_or_update_graph` - -`build_or_update_graph`는 두 단계로 구성됩니다: - -1. 그래프 빌드/업데이트 -2. `postprocess`에 의해 제어되는 선택적 사후 처리 작업 - -빌드/업데이트 단계 자체가 실패하면, 도구는 사후 처리 상태를 생성하기 전에 에러를 반환합니다. - -### 입력 검증 및 하드 실패 (Hard failures) - -| 조건 | 내부 동작 | 결과 | -|-----------|-------------------|--------| -| `path` 누락 | 실행 전 요청 검증 실패 | 에러 | -| `path`가 설정된 분석 루트 외부임 | 경로 검증 실패 | 에러 | -| `postprocess`가 `full`, `minimal`, `none` 중 하나가 아님 | 요청 검증 실패 | 에러 | -| `postprocess_policy`가 `degraded`, `fail_closed` 중 하나가 아님 | 요청 검증 실패 | 에러 | -| 그래프 빌드/업데이트 실패 | 빌드 또는 증분 업데이트 에러 반환 | 에러 | - -### 사후 처리 모드 동작 - -| `postprocess` 값 | 시도되는 단계 | 설계상 건너뛰는 단계 | -|---------------------|-----------------|--------------------------| -| `full` | `flows`, `communities`, `search_documents`, `fts` | 없음 | -| `minimal` | `search_documents`, `fts` | `flows`, `communities` | -| `none` | 없음 | `flows`, `communities`, `search_documents`, `fts` | - -### 상태 표 (Status table) - -| 상황 | 예시 | 반환 결과 | -|-----------|---------|-----------------| -| 빌드/업데이트 성공 및 요청된 모든 사후 처리 단계 성공 | 전체 재빌드 + 활성화된 모든 파생 상태 갱신 완료 | `status="ok"` | -| 요청된 단계가 실패했으나 유효 정책이 `degraded`임 | `communities` 재생성 실패 | `status="degraded"`, `failed_steps`에 `communities` 포함 | -| 요청된 단계가 실패했고 유효 정책이 `fail_closed`임 | 자동 격상 또는 명시적 재정의 후 `fts` 재생성 실패 | 도구가 해당 호출에 대해 에러 반환 | -| 요청된 단계를 사용할 수 없음 | `FlowBuilder == nil` 또는 `SearchBackend == nil` | `skipped_steps`에 표시됨; 그 자체로 에러는 아님 | -| 선택된 모드에서 특정 단계를 제외함 | `postprocess="minimal"` 또는 `postprocess="none"` | `skipped_steps`에 표시됨; 그 자체로 에러는 아님 | - -### 단계별 실패 매핑 - -| 단계 | 실패 원인 | `degraded` 정책 결과 | `fail_closed` 정책 결과 | -|------|---------------|--------------------------|-----------------------------| -| `flows` | `FlowBuilder.Rebuild()` 에러 반환 | `failed_steps += ["flows"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `communities` | `CommunityBuilder.Rebuild()` 에러 반환 | `failed_steps += ["communities"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `search_documents` | 검색 문서 갱신 실패 | `failed_steps += ["search_documents"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `fts` | `SearchBackend.Rebuild()` 에러 반환 | `failed_steps += ["fts"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | - -### 확인해야 할 응답 필드 - -| 필드 | 의미 | -|-------|---------| -| `status` | 전체 결과: `ok` 또는 `degraded` | -| `postprocess_policy` | 호출에 사용된 유효 정책 | -| `policy_source` | 호출자가 정책을 제공한 경우 `explicit`, 그 외에는 `auto` | -| `failed_steps` | 실행되었으나 실패한 요청 단계들 | -| `skipped_steps` | 의도적으로 실행되지 않은 요청 또는 모드 제어 단계들 | - -## `run_postprocess` - -`run_postprocess`는 이미 빌드된 그래프 상태에서만 작동합니다. 소스 파일을 다시 파싱하지 않습니다. - -### 입력 검증 및 하드 실패 - -| 조건 | 내부 동작 | 결과 | -|-----------|-------------------|--------| -| `community_depth`가 `1..8` 범위를 벗어남 | 요청 검증 실패 | 에러 | -| `postprocess_policy`가 `degraded`, `fail_closed` 중 하나가 아님 | 요청 검증 실패 | 에러 | - -### 요청 단계 동작 - -| 요청 플래그 | 시도되는 단계 | 건너뛰는 단계 | -|---------------|-----------------|---------------| -| `flows=true` | `flows` | `FlowBuilder == nil`인 경우 제외하고 없음 | -| `communities=true` | `communities` | `CommunityBuilder == nil`인 경우 제외하고 없음 | -| `fts=true` | `search_documents`, `fts` | `SearchBackend == nil`인 경우 제외하고 없음 | -| `flows=false` | 없음 | `flows` | -| `communities=false` | 없음 | `communities` | -| `fts=false` | 없음 | `search_documents`, `fts` | - -### 상태 표 - -| 상황 | 예시 | 반환 결과 | -|-----------|---------|-----------------| -| 요청된 모든 단계 성공 | `flows=true, communities=true, fts=true` 및 모든 재생성 성공 | `status="ok"` | -| 요청된 단계가 실패했으나 유효 정책이 `degraded`임 | `search_documents` 갱신 실패 | `status="degraded"`, 실패한 단계 보고됨 | -| 요청된 단계가 실패했고 유효 정책이 `fail_closed`임 | 명시적 또는 자동 fail-closed 정책 하에 `flows` 재생성 실패 | 도구가 해당 호출에 대해 에러 반환 | -| 요청된 단계를 사용할 수 없음 | `CommunityBuilder == nil` 또는 `SearchBackend == nil` | `skipped_steps`에 표시됨; 그 자체로 에러는 아님 | -| 호출자가 단계를 비활성화함 | `flows=false` 또는 `fts=false` | `skipped_steps`에 표시됨; 그 자체로 에러는 아님 | - -### 단계별 실패 매핑 - -| 단계 | 실패 원인 | `degraded` 정책 결과 | `fail_closed` 정책 결과 | -|------|---------------|--------------------------|-----------------------------| -| `flows` | `FlowBuilder.Rebuild()` 에러 반환 | `failed_steps += ["flows"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `communities` | `CommunityBuilder.Rebuild()` 에러 반환 | `failed_steps += ["communities"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `search_documents` | 검색 문서 갱신 실패 | `failed_steps += ["search_documents"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | -| `fts` | `SearchBackend.Rebuild()` 에러 반환 | `failed_steps += ["fts"]`, JSON 결과는 반환됨 | 실패가 기록된 후 도구가 에러 반환 | - -### 확인해야 할 응답 필드 - -| 필드 | 의미 | -|-------|---------| -| `status` | 전체 결과: `ok` 또는 `degraded` | -| `postprocess_policy` | 호출에 사용된 유효 정책 | -| `policy_source` | 호출자가 정책을 제공한 경우 `explicit`, 그 외에는 `auto` | -| `failed_steps` | 실행되었으나 실패한 요청 단계들 | -| `skipped_steps` | 비활성화되었거나 사용할 수 없는 단계들 | -| `flows_count` | 이번 실행에서 재생성된 저장된 흐름의 수 | -| `communities_count` | 이번 실행에서 재생성된 커뮤니티의 수 | -| `fts_indexed` | FTS 재생성이 완료된 경우 `1`, 그렇지 않으면 `0` | - -## 운영 관련 읽기 (Operational reading) - -| 관찰 사항 | 의미 | 일반적인 다음 조치 | -|-------------|---------|---------------------| -| `status="degraded"` | 요청은 완료되었으나, 일부 파생 상태가 오래되었거나 부분적으로만 갱신되었을 수 있음 | `failed_steps`를 조사하고, 실패한 백엔드/빌더/설정을 수정한 후 도구 재실행 | -| `fail_closed` 하에서 도구가 에러 반환 | 유효 정책상 해당 호출에서 실패를 용납할 수 없는 것으로 판단함 | 근본적인 실패 원인을 수정하고 재실행; 필요한 경우 일시적으로 명시적인 `postprocess_policy="degraded"` 사용 | -| `skipped_steps`가 비어 있지 않음 | 요청된 작업이 의도적으로 시도되지 않음 | 호출자가 단계를 비활성화했는지 또는 필요한 백엔드/빌더가 누락되었는지 확인 | -| 자동 정책이 `fail_closed`로 전환됨 | 동일한 `(namespace, tool)`에 대한 최근 실패가 격상 임계값을 초과함 | 이를 일회성 경고가 아닌 지속적인 운영 문제로 취급하여 조치 | diff --git a/guide/ko/webhook.md b/guide/ko/webhook.md index 3df1d8b..376028a 100644 --- a/guide/ko/webhook.md +++ b/guide/ko/webhook.md @@ -230,7 +230,7 @@ CCG는 현재 웹훅 운영을 위한 `/metrics` 엔드포인트를 제공하지 ```bash ccg update /data/repos/api --namespace api - ccg status --namespace api --errors + ccg status --namespace api ``` 5. 검색, 커뮤니티, 저장된 flow가 여전히 오래된 것처럼 보이면 namespace `api`에 대해 필요한 postprocess 플래그와 함께 MCP `run_postprocess` 도구를 호출합니다. diff --git a/guide/lint.md b/guide/lint.md index 804e9a5..074c4c8 100644 --- a/guide/lint.md +++ b/guide/lint.md @@ -265,4 +265,3 @@ Typical uses: - [CLI Reference](cli-reference.md#lint-categories) - [Custom Annotations](annotations.md) - [Development](development.md) -- [Postprocess Failure Policy](postprocess-failure-policy.md) diff --git a/guide/mcp-tools.md b/guide/mcp-tools.md index 896658c..1a6f217 100644 --- a/guide/mcp-tools.md +++ b/guide/mcp-tools.md @@ -41,27 +41,18 @@ Start the self-hosted server with `ccg-server`; clients connect to its `/mcp` en |------|-------------| | `parse_project` | Parse source files | | `build_or_update_graph` | Full/incremental build with postprocessing | -| `run_postprocess` | Rebuild stored flows, communities, and/or full-text search derived state | -| `get_postprocess_policy` | Inspect automatic postprocess policy state and recent failures | -| `reset_postprocess_policy` | Record a reset marker to clear fail-closed streak for one tool | +| `run_postprocess` | Rebuild stored flows and/or full-text search derived state | | `get_node` | Get node by qualified name | | `search` | Full-text search | | `query_graph` | Predefined graph queries (callers, callees, imports, etc.) | | `list_graph_stats` | Node/edge/file counts | -| `get_minimal_context` | Lightweight summary (~100 tokens) for AI agent entry point — graph stats, risk, top communities/flows, tool suggestions | +| `get_minimal_context` | Lightweight summary (~100 tokens) for AI agent entry point — graph stats, risk, top flows, tool suggestions | -`build_or_update_graph` and `run_postprocess` both support an automatic -postprocess failure policy. When no explicit `postprocess_policy` is provided, -CCG defaults to `degraded` and automatically escalates to `fail_closed` after -three consecutive `degraded` runs for the same `(namespace, tool)` pair. - -See [Postprocess Failure Policy](postprocess-failure-policy.md) for the detailed -status tables, failure causes, skip behavior, and policy escalation rules for -`build_or_update_graph` and `run_postprocess`. - -CCG does not expose a Prometheus `/metrics` endpoint yet. For postprocess -operations, use `get_postprocess_policy` and the HTTP `/status` summary as the -current machine-readable operational surfaces. +`build_or_update_graph` and `run_postprocess` run derived-state steps (stored +flows, full-text search) after the graph write. When a step fails, the tool +still returns a structured result with `status: "degraded"` and the failing +steps listed in `failed_steps`; the failure is also logged. Inspect that +response (or the HTTP `/status` summary) to detect degraded postprocessing. ### Analysis diff --git a/guide/operations.md b/guide/operations.md index 7a14702..04e13a1 100644 --- a/guide/operations.md +++ b/guide/operations.md @@ -261,7 +261,7 @@ Recommended checks: | Signal | Meaning | Operator Action | |--------|---------|-----------------| | `/ready` returns `not_ready` | DB unavailable, queue full, or blocking queue age | Stop sending traffic and inspect logs/status. | -| `/status` is `degraded` | Last webhook or postprocess state needs attention | Inspect failed repo/config and retry with a new push or manual update. | +| `/status` is `degraded` | Last webhook state needs attention | Inspect failed repo/config and retry with a new push or manual update. | | Queue age grows steadily | Workers cannot keep up with incoming pushes | Reduce repo scope, increase workers on PostgreSQL, or split namespaces. | | Search results look stale | Search postprocess may have failed or been skipped | Run `run_postprocess` with `fts=true` or rebuild/update the namespace. | @@ -299,7 +299,7 @@ Manual recovery: ```bash ccg update /data/repos/api --namespace api -ccg status --namespace api --errors +ccg status --namespace api ``` If search, communities, or saved flows still look stale, call the MCP @@ -341,6 +341,6 @@ existing schema and migrated explicitly before restarting runtime commands. | Webhook returns forbidden | Repo or branch not allowed | Check `--allow-repo` patterns and branch refs. | | Webhook returns too many requests | Sync queue is full | Check `/status`, reduce push volume, or increase workers on PostgreSQL. | | `/ready` is `not_ready` | DB or queue blocking condition | Inspect `/status` and service logs. | -| `/status` is `degraded` | Last webhook or postprocess failed | Fix repo config or rerun update/postprocess. | +| `/status` is `degraded` | Last webhook failed | Fix repo config or rerun update/postprocess. | | Search misses recent code | FTS/search documents stale | Run `run_postprocess` with `fts=true` or rebuild the namespace. | | Migration error at startup | Schema version mismatch, migration source mismatch, or schema drift | Run `ccg migrate` from the deployed binary version; if it still fails, verify the configured migration source and schema drift. | diff --git a/guide/postprocess-failure-policy.md b/guide/postprocess-failure-policy.md deleted file mode 100644 index e58f2c9..0000000 --- a/guide/postprocess-failure-policy.md +++ /dev/null @@ -1,190 +0,0 @@ -# Postprocess Failure Policy - -This guide explains how CCG reports `ok`, `degraded`, `fail_closed`, and -`skipped_steps` for the two MCP tools that can rebuild derived graph state: - -- `build_or_update_graph` -- `run_postprocess` - -It also explains the automatic policy engine that chooses `degraded` or -`fail_closed` when the caller does not provide an explicit -`postprocess_policy`. - -## Terms - -| Term | Meaning | -|------|---------| -| `ok` | Requested work succeeded, or only skipped steps remained. | -| `degraded` | Some requested postprocess steps failed, but the tool still returned a structured success result. | -| `fail_closed` | A failure policy that does not tolerate postprocess step errors. When a requested step fails, the tool returns an error for that call. | -| `skipped_steps` | Steps that were not attempted because the caller disabled them, the selected mode did not include them, or the required builder/backend was not configured. | - -## Automatic policy engine - -When the caller omits `postprocess_policy`, CCG resolves the effective policy in -this order: - -1. Explicit caller value wins when provided. -2. Otherwise, the automatic policy engine looks at recent runs for the same - `(namespace, tool)` pair. -3. Default policy is `degraded`. -4. After **three consecutive `degraded` runs** for the same `(namespace, tool)`, the - automatic policy escalates to `fail_closed`. -5. A later `ok` run resets the consecutive-failure streak. - -Policy state is persisted as: - -- current effective state in `ccg_postprocess_policy_state` -- append-only execution history in `ccg_postprocess_run_logs` - -For operators, CCG now exposes two lightweight control surfaces: - -- `get_postprocess_policy` returns the current fail-closed entries and recent failures. -- `reset_postprocess_policy` records a reset marker run for one tool in the current namespace. - -The reset path does not delete execution history. It inserts an `ok` marker with -source `reset`, which safely breaks the consecutive-failure streak used by the -automatic resolver. - -Run logs are also pruned opportunistically after writes so each `(namespace, -tool)` keeps only a bounded recent history instead of growing forever. - -## Namespace isolation and shared state - -Automatic policy decisions and postprocess results are scoped to the current -`namespace`. - -- Policy escalation is tracked per `(namespace, tool)`. -- Derived state rebuilds (`flows`, `communities`, `search_documents`, `fts`) are - applied only to the active namespace. -- A failure in one namespace does not directly mark another namespace as - `degraded` or `fail_closed`. - -In normal operation, this means a failed rebuild in namespace `A` can leave only -namespace `A` stale while namespace `B` continues to query normally. - -The main exception is truly global operational state, such as schema migration -compatibility. For example, `SchemaVersion` is not namespace-scoped, so a global -schema mismatch can affect the whole deployment even though postprocess policy -and derived graph state are isolated per namespace. - -## `build_or_update_graph` - -`build_or_update_graph` has two phases: - -1. graph build/update -2. optional postprocess work controlled by `postprocess` - -If the build/update phase itself fails, the tool returns an error before any -postprocess status is produced. - -### Input validation and hard failures - -| Condition | Internal behavior | Result | -|-----------|-------------------|--------| -| `path` is missing | Request validation fails before execution | Error | -| `path` is outside the configured analysis root | Path validation fails | Error | -| `postprocess` is not `full`, `minimal`, or `none` | Request validation fails | Error | -| `postprocess_policy` is not `degraded` or `fail_closed` | Request validation fails | Error | -| Graph build/update fails | Build or incremental update returns an error | Error | - -### Postprocess mode behavior - -| `postprocess` value | Steps attempted | Steps skipped by design | -|---------------------|-----------------|--------------------------| -| `full` | `flows`, `communities`, `search_documents`, `fts` | none | -| `minimal` | `search_documents`, `fts` | `flows`, `communities` | -| `none` | none | `flows`, `communities`, `search_documents`, `fts` | - -### Status table - -| Situation | Example | Returned result | -|-----------|---------|-----------------| -| Build/update succeeded and all requested postprocess steps succeeded | Full rebuild + all enabled derived state refreshed | `status="ok"` | -| Requested step failed and effective policy is `degraded` | `communities` rebuild fails | `status="degraded"`, `failed_steps` contains `communities` | -| Requested step failed and effective policy is `fail_closed` | `fts` rebuild fails after auto escalation or explicit override | Tool returns an error for the call | -| Requested step is unavailable | `FlowBuilder == nil` or `SearchBackend == nil` | Step appears in `skipped_steps`; not an error by itself | -| Selected mode excludes a step | `postprocess="minimal"` or `postprocess="none"` | Step appears in `skipped_steps`; not an error by itself | - -### Step-by-step failure map - -| Step | Failure cause | `degraded` policy result | `fail_closed` policy result | -|------|---------------|--------------------------|-----------------------------| -| `flows` | `FlowBuilder.Rebuild()` returns an error | `failed_steps += ["flows"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `communities` | `CommunityBuilder.Rebuild()` returns an error | `failed_steps += ["communities"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `search_documents` | search document refresh fails | `failed_steps += ["search_documents"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `fts` | `SearchBackend.Rebuild()` returns an error | `failed_steps += ["fts"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | - -### Response fields to inspect - -| Field | Meaning | -|-------|---------| -| `status` | Overall result: `ok` or `degraded` | -| `postprocess_policy` | Effective policy used for the call | -| `policy_source` | `explicit` when caller supplied the policy, `auto` otherwise | -| `failed_steps` | Requested steps that ran and failed | -| `skipped_steps` | Requested or mode-controlled steps that were intentionally not run | - -## `run_postprocess` - -`run_postprocess` operates only on already-built graph state. It does not parse -source files again. - -### Input validation and hard failures - -| Condition | Internal behavior | Result | -|-----------|-------------------|--------| -| `community_depth` is outside `1..8` | Request validation fails | Error | -| `postprocess_policy` is not `degraded` or `fail_closed` | Request validation fails | Error | - -### Requested-step behavior - -| Request flags | Steps attempted | Steps skipped | -|---------------|-----------------|---------------| -| `flows=true` | `flows` | none unless `FlowBuilder == nil` | -| `communities=true` | `communities` | none unless `CommunityBuilder == nil` | -| `fts=true` | `search_documents`, `fts` | none unless `SearchBackend == nil` | -| `flows=false` | none | `flows` | -| `communities=false` | none | `communities` | -| `fts=false` | none | `search_documents`, `fts` | - -### Status table - -| Situation | Example | Returned result | -|-----------|---------|-----------------| -| All requested steps succeeded | `flows=true, communities=true, fts=true` and all rebuilds succeed | `status="ok"` | -| Requested step failed and effective policy is `degraded` | `search_documents` refresh fails | `status="degraded"`, failed step is reported | -| Requested step failed and effective policy is `fail_closed` | `flows` rebuild fails after explicit or automatic fail-closed policy | Tool returns an error for the call | -| Requested step is unavailable | `CommunityBuilder == nil` or `SearchBackend == nil` | Step appears in `skipped_steps`; not an error by itself | -| Caller disabled a step | `flows=false` or `fts=false` | Step appears in `skipped_steps`; not an error by itself | - -### Step-by-step failure map - -| Step | Failure cause | `degraded` policy result | `fail_closed` policy result | -|------|---------------|--------------------------|-----------------------------| -| `flows` | `FlowBuilder.Rebuild()` returns an error | `failed_steps += ["flows"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `communities` | `CommunityBuilder.Rebuild()` returns an error | `failed_steps += ["communities"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `search_documents` | search document refresh fails | `failed_steps += ["search_documents"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | -| `fts` | `SearchBackend.Rebuild()` returns an error | `failed_steps += ["fts"]`, tool still returns JSON result | Failure is recorded, then the tool returns an error | - -### Response fields to inspect - -| Field | Meaning | -|-------|---------| -| `status` | Overall result: `ok` or `degraded` | -| `postprocess_policy` | Effective policy used for the call | -| `policy_source` | `explicit` when caller supplied the policy, `auto` otherwise | -| `failed_steps` | Requested steps that ran and failed | -| `skipped_steps` | Disabled or unavailable steps | -| `flows_count` | Number of stored flows rebuilt in this run | -| `communities_count` | Number of communities rebuilt in this run | -| `fts_indexed` | `1` when FTS rebuild completed, otherwise `0` | - -## Operational reading - -| Observation | Meaning | Typical next action | -|-------------|---------|---------------------| -| `status="degraded"` | The request completed, but some derived state may now be stale or partially refreshed | Inspect `failed_steps`, fix the failing backend/builder/config, then rerun the tool | -| Tool returned an error under `fail_closed` | The effective policy considered the failure non-tolerable for that call | Fix the underlying failure and rerun; if needed, temporarily use explicit `postprocess_policy="degraded"` | -| `skipped_steps` is non-empty | Requested work was intentionally not attempted | Check whether the caller disabled the step or whether the required backend/builder is missing | -| Auto policy switched to `fail_closed` | Recent failures for the same `(namespace, tool)` crossed the escalation threshold | Treat it as a persistent operational problem, not a one-off transient warning | diff --git a/guide/webhook.md b/guide/webhook.md index b02e882..36ef631 100644 --- a/guide/webhook.md +++ b/guide/webhook.md @@ -274,7 +274,7 @@ growing, or a deployment restart may have interrupted an accepted event. ```bash ccg update /data/repos/api --namespace api - ccg status --namespace api --errors + ccg status --namespace api ``` 5. If search, communities, or saved flows still look stale, call the MCP diff --git a/internal/cli/status.go b/internal/cli/status.go index f948fff..8ec1c23 100644 --- a/internal/cli/status.go +++ b/internal/cli/status.go @@ -1,19 +1,14 @@ package cli import ( - "context" "fmt" - "io" "math" - "strings" "github.com/spf13/cobra" "github.com/tae2089/trace" - "gorm.io/gorm" "github.com/tae2089/code-context-graph/internal/ctxns" "github.com/tae2089/code-context-graph/internal/model" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" ) // kindCount holds grouped count rows for graph statistics queries. @@ -33,9 +28,6 @@ const ( // @requires deps.DB가 초기화되어 있어야 한다. // @sideEffect 데이터베이스를 조회하고 통계를 출력한다. func newStatusCmd(deps *Deps) *cobra.Command { - var showErrors bool - var recentLimit int - cmd := &cobra.Command{ Use: "status", Short: "Show graph statistics", @@ -44,9 +36,6 @@ func newStatusCmd(deps *Deps) *cobra.Command { if deps.DB == nil { return errDBNotInitialized } - if recentLimit <= 0 { - return fmt.Errorf("recent must be > 0, got %d", recentLimit) - } ns, _ := cmd.Flags().GetString("namespace") ctx := ctxns.WithNamespace(cmd.Context(), ns) @@ -128,68 +117,13 @@ func newStatusCmd(deps *Deps) *cobra.Command { } } - if err := printPostprocessStatus(ctx, out, deps.DB, ctxns.FromContext(ctx), recentLimit, showErrors); err != nil { - return trace.Wrap(err, "postprocess status") - } - return nil }, } - cmd.Flags().BoolVar(&showErrors, "errors", false, "Show recent postprocess failure details") - cmd.Flags().IntVar(&recentLimit, "recent", postprocesspolicy.DefaultStatusLimit, "Number of recent postprocess failures to inspect") - return cmd } -// printPostprocessStatus adds persisted postprocess policy health to graph stats. -// @intent surface degraded derived-state failures from the same CLI status command operators already use. -func printPostprocessStatus(ctx context.Context, out io.Writer, db *gorm.DB, namespace string, recentLimit int, showErrors bool) error { - summary, err := postprocesspolicy.NewStore(db).Status(ctx, postprocesspolicy.StatusOptions{ - Namespace: namespace, - RecentLimit: recentLimit, - }) - if err != nil { - return err - } - - fmt.Fprintln(out, "\nPostprocess:") - fmt.Fprintf(out, " Status: %s\n", summary.Status) - if !showErrors { - fmt.Fprintf(out, " Fail-closed: %d\n", len(summary.FailClosed)) - fmt.Fprintf(out, " Recent failures: %d\n", len(summary.RecentFailures)) - return nil - } - - if len(summary.FailClosed) > 0 { - fmt.Fprintln(out, "\nFail-closed:") - for _, state := range summary.FailClosed { - fmt.Fprintf(out, " %s consecutive_failures=%d updated_at=%s\n", - state.Tool, state.ConsecutiveFailures, state.UpdatedAt.UTC().Format(timeFormatRFC3339)) - } - } - - if len(summary.RecentFailures) > 0 { - fmt.Fprintln(out, "\nRecent failures:") - for _, run := range summary.RecentFailures { - fmt.Fprintf(out, " %s policy=%s failed_steps=%s skipped_steps=%s created_at=%s\n", - run.Tool, run.Policy, formatStatusList(run.FailedSteps), formatStatusList(run.SkippedSteps), run.CreatedAt.UTC().Format(timeFormatRFC3339)) - } - } - - return nil -} - -const timeFormatRFC3339 = "2006-01-02T15:04:05Z07:00" - -// @intent 최근 실패 step 목록을 status 출력에 compact한 한 줄 문자열로 직렬화한다. -func formatStatusList(values []string) string { - if len(values) == 0 { - return "[]" - } - return strings.Join(values, ",") -} - // @intent compute the share of fallback call edges within all call-like edges for operator-facing health reporting. func callFallbackRatio(strictCalls, fallbackCalls int64) float64 { total := strictCalls + fallbackCalls diff --git a/internal/cli/status_test.go b/internal/cli/status_test.go index 1ee0ffc..db72db4 100644 --- a/internal/cli/status_test.go +++ b/internal/cli/status_test.go @@ -2,10 +2,8 @@ package cli import ( "bytes" - "context" "strings" "testing" - "time" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -14,7 +12,6 @@ import ( "github.com/tae2089/code-context-graph/internal/ctxns" "github.com/tae2089/code-context-graph/internal/model" "github.com/tae2089/code-context-graph/internal/parse/treesitter" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" "github.com/tae2089/code-context-graph/internal/store/gormstore" ) @@ -321,169 +318,3 @@ func TestStatusCommand_RespectsNamespace(t *testing.T) { t.Fatalf("unexpected cross-namespace aggregation: %s", out) } } - -func TestStatusCommand_ShowsPostprocessOKSummary(t *testing.T) { - deps, stdout, stderr, db := setupStatusTest(t) - - if err := db.Create(&model.Node{Namespace: ctxns.DefaultNamespace, QualifiedName: "default.Foo", Kind: model.NodeKindFunction, Name: "Foo", FilePath: "default/foo.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil { - t.Fatal(err) - } - - stdout.Reset() - stderr.Reset() - - if err := executeCmd(deps, stdout, stderr, "status"); err != nil { - t.Fatalf("status: %v", err) - } - - out := stdout.String() - for _, want := range []string{ - "Postprocess:", - "Status: ok", - "Fail-closed: 0", - "Recent failures: 0", - } { - if !strings.Contains(out, want) { - t.Fatalf("expected %q in output, got: %s", want, out) - } - } -} - -func TestStatusCommand_ShowsPostprocessErrors(t *testing.T) { - deps, stdout, stderr, db := setupStatusTest(t) - - if err := db.Create(&model.Node{Namespace: ctxns.DefaultNamespace, QualifiedName: "default.Foo", Kind: model.NodeKindFunction, Name: "Foo", FilePath: "default/foo.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil { - t.Fatal(err) - } - - store := postprocesspolicy.NewStore(db) - ctx := ctxns.WithNamespace(context.Background(), ctxns.DefaultNamespace) - base := time.Date(2026, 5, 4, 10, 0, 0, 0, time.UTC) - for i := 0; i < 3; i++ { - if err := store.RecordRun(ctx, postprocesspolicy.RunRecord{ - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocesspolicy.PolicyFailClosed, - Source: postprocesspolicy.SourceAuto, - Status: postprocesspolicy.StatusDegraded, - FailedSteps: []string{"communities"}, - SkippedSteps: []string{"fts"}, - CreatedAt: base.Add(time.Duration(i) * time.Minute), - }); err != nil { - t.Fatal(err) - } - } - - stdout.Reset() - stderr.Reset() - - if err := executeCmd(deps, stdout, stderr, "status", "--errors"); err != nil { - t.Fatalf("status: %v", err) - } - - out := stdout.String() - for _, want := range []string{ - "Status: degraded", - "Fail-closed:", - "run_postprocess consecutive_failures=3", - "Recent failures:", - "policy=fail_closed", - "failed_steps=communities", - "skipped_steps=fts", - } { - if !strings.Contains(out, want) { - t.Fatalf("expected %q in output, got: %s", want, out) - } - } -} - -func TestStatusCommand_RecentLimitsPostprocessFailures(t *testing.T) { - deps, stdout, stderr, db := setupStatusTest(t) - - if err := db.Create(&model.Node{Namespace: ctxns.DefaultNamespace, QualifiedName: "default.Foo", Kind: model.NodeKindFunction, Name: "Foo", FilePath: "default/foo.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil { - t.Fatal(err) - } - - store := postprocesspolicy.NewStore(db) - ctx := ctxns.WithNamespace(context.Background(), ctxns.DefaultNamespace) - base := time.Date(2026, 5, 4, 10, 0, 0, 0, time.UTC) - for i, step := range []string{"communities", "flows"} { - if err := store.RecordRun(ctx, postprocesspolicy.RunRecord{ - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocesspolicy.PolicyDegraded, - Source: postprocesspolicy.SourceAuto, - Status: postprocesspolicy.StatusDegraded, - FailedSteps: []string{step}, - CreatedAt: base.Add(time.Duration(i) * time.Minute), - }); err != nil { - t.Fatal(err) - } - } - - stdout.Reset() - stderr.Reset() - - if err := executeCmd(deps, stdout, stderr, "status", "--errors", "--recent", "1"); err != nil { - t.Fatalf("status: %v", err) - } - - out := stdout.String() - if !strings.Contains(out, "failed_steps=flows") { - t.Fatalf("expected newest failure in output, got: %s", out) - } - if strings.Contains(out, "failed_steps=communities") { - t.Fatalf("expected older failure to be omitted, got: %s", out) - } -} - -func TestStatusCommand_PostprocessErrorsRespectNamespace(t *testing.T) { - deps, stdout, stderr, db := setupStatusTest(t) - - for _, ns := range []string{ctxns.DefaultNamespace, "other"} { - if err := db.Create(&model.Node{Namespace: ns, QualifiedName: ns + ".Foo", Kind: model.NodeKindFunction, Name: "Foo", FilePath: ns + "/foo.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil { - t.Fatal(err) - } - } - - store := postprocesspolicy.NewStore(db) - otherCtx := ctxns.WithNamespace(context.Background(), "other") - if err := store.RecordRun(otherCtx, postprocesspolicy.RunRecord{ - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocesspolicy.PolicyDegraded, - Source: postprocesspolicy.SourceAuto, - Status: postprocesspolicy.StatusDegraded, - FailedSteps: []string{"communities"}, - CreatedAt: time.Date(2026, 5, 4, 10, 0, 0, 0, time.UTC), - }); err != nil { - t.Fatal(err) - } - - stdout.Reset() - stderr.Reset() - - if err := executeCmd(deps, stdout, stderr, "status"); err != nil { - t.Fatalf("status: %v", err) - } - - out := stdout.String() - if !strings.Contains(out, "Status: ok") { - t.Fatalf("expected default namespace postprocess status ok, got: %s", out) - } - if strings.Contains(out, "communities") { - t.Fatalf("unexpected cross-namespace failure in output: %s", out) - } -} - -func TestStatusCommand_RejectsInvalidRecent(t *testing.T) { - deps, stdout, stderr, _ := setupStatusTest(t) - - stdout.Reset() - stderr.Reset() - - err := executeCmd(deps, stdout, stderr, "status", "--recent", "0") - if err == nil { - t.Fatal("expected invalid recent error") - } - if !strings.Contains(err.Error(), "recent must be > 0") { - t.Fatalf("unexpected error: %v", err) - } -} diff --git a/internal/db/migration/migration.go b/internal/db/migration/migration.go index 1e6d096..6534b47 100644 --- a/internal/db/migration/migration.go +++ b/internal/db/migration/migration.go @@ -24,7 +24,7 @@ import ( ) const ( - RequiredSchemaVersion = 6 + RequiredSchemaVersion = 7 SchemaVersionKey = "schema" LegacySchemaVersionTable = "ccg_schema_versions" ) @@ -59,14 +59,6 @@ type SchemaColumn struct { Column string } -// SchemaTypeCheck represents a required column type assertion. -// @intent 특정 컬럼의 데이터 타입까지 검증해야 하는 조건을 표현한다. -type SchemaTypeCheck struct { - Table string - Column string - DataType string -} - // Run executes all pending migrations and validates schema parity. // @intent 마이그레이션 실행과 사후 스키마 정합성 검사를 하나의 진입점으로 묶는다. // @sideEffect 데이터베이스 스키마와 migration metadata를 변경할 수 있다. @@ -390,8 +382,6 @@ func RequiredSchemaTables() []string { "community_memberships", "flows", "flow_memberships", - "ccg_postprocess_policy_state", - "ccg_postprocess_run_logs", "search_documents", } } @@ -414,18 +404,6 @@ func ModelNullabilityColumns() []SchemaColumn { {Table: "flows", Column: "name"}, {Table: "flow_memberships", Column: "flow_id"}, {Table: "flow_memberships", Column: "node_id"}, - {Table: "ccg_postprocess_policy_state", Column: "namespace"}, - {Table: "ccg_postprocess_policy_state", Column: "tool"}, - {Table: "ccg_postprocess_policy_state", Column: "policy"}, - {Table: "ccg_postprocess_policy_state", Column: "updated_at"}, - {Table: "ccg_postprocess_run_logs", Column: "namespace"}, - {Table: "ccg_postprocess_run_logs", Column: "tool"}, - {Table: "ccg_postprocess_run_logs", Column: "policy"}, - {Table: "ccg_postprocess_run_logs", Column: "source"}, - {Table: "ccg_postprocess_run_logs", Column: "status"}, - {Table: "ccg_postprocess_run_logs", Column: "failed_steps"}, - {Table: "ccg_postprocess_run_logs", Column: "skipped_steps"}, - {Table: "ccg_postprocess_run_logs", Column: "created_at"}, } } @@ -607,18 +585,6 @@ func validatePostgresSchemaParity(db *gorm.DB) error { } else if !ok { return fmt.Errorf("required trigger %q is missing", "trg_search_documents_tsv") } - for _, tc := range []SchemaTypeCheck{ - {Table: "ccg_postprocess_run_logs", Column: "failed_steps", DataType: "jsonb"}, - {Table: "ccg_postprocess_run_logs", Column: "skipped_steps", DataType: "jsonb"}, - } { - dataType, err := postgresColumnDataType(db, tc.Table, tc.Column) - if err != nil { - return trace.Wrap(err, "inspect postgres column type") - } - if dataType != tc.DataType { - return fmt.Errorf("required column %q.%q type is %q, want %q", tc.Table, tc.Column, dataType, tc.DataType) - } - } for _, indexName := range []string{"idx_edges_ns_from_kind_to", "idx_edges_ns_to_kind_from"} { exists, err := postgresIndexExists(db, indexName) if err != nil { diff --git a/internal/mcp/deps.go b/internal/mcp/deps.go index 88f669e..3fac780 100644 --- a/internal/mcp/deps.go +++ b/internal/mcp/deps.go @@ -14,7 +14,6 @@ import ( "github.com/tae2089/code-context-graph/internal/analysis/incremental" "github.com/tae2089/code-context-graph/internal/analysis/query" "github.com/tae2089/code-context-graph/internal/model" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" "github.com/tae2089/code-context-graph/internal/store" storesearch "github.com/tae2089/code-context-graph/internal/store/search" ) @@ -90,15 +89,6 @@ type IncrementalSyncer interface { SyncWithExisting(ctx context.Context, files map[string]incremental.FileInfo, existingFiles []string) (*incremental.SyncStats, error) } -// PostprocessPolicy gates and records automatic postprocess runs so repeated failures can be detected and suppressed. -// @intent centralize automatic postprocess decisions so repeated failures can degrade execution before handlers retry expensive work. -type PostprocessPolicy interface { - Resolve(ctx context.Context, input postprocesspolicy.DecisionInput) (string, string, error) - RecordRun(ctx context.Context, record postprocesspolicy.RunRecord) error - Status(ctx context.Context, opts postprocesspolicy.StatusOptions) (*postprocesspolicy.StatusSummary, error) - Reset(ctx context.Context, tool string) error -} - // Deps collects the services and stores required by MCP handlers. // @intent Assembles tool and prompt handlers by injecting all MCP server components at once. type Deps struct { @@ -112,11 +102,10 @@ type Deps struct { Logger *slog.Logger // Added in Phase 11 - QueryService QueryService - FallbackAnalyzer FallbackAnalyzer - FlowBuilder FlowBuilder - Incremental IncrementalSyncer - PostprocessPolicy PostprocessPolicy + QueryService QueryService + FallbackAnalyzer FallbackAnalyzer + FlowBuilder FlowBuilder + Incremental IncrementalSyncer // Cache — nil disables caching Cache *Cache diff --git a/internal/mcp/handler_parse.go b/internal/mcp/handler_parse.go index e4b5882..c8bea9b 100644 --- a/internal/mcp/handler_parse.go +++ b/internal/mcp/handler_parse.go @@ -12,7 +12,6 @@ import ( flowspkg "github.com/tae2089/code-context-graph/internal/analysis/flows" "github.com/tae2089/code-context-graph/internal/obs" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" "github.com/tae2089/code-context-graph/internal/safepath" "github.com/tae2089/code-context-graph/internal/service" ) @@ -27,27 +26,23 @@ func (h *handlers) refreshSearchDocuments(ctx context.Context) (int, error) { // @intent serialize build_or_update_graph results with a fixed JSON schema without changing the wire format. type buildOrUpdateGraphResponse struct { - Status string `json:"status"` - FilesParsed int `json:"files_parsed"` - NodesCreated int `json:"nodes_created"` - EdgesCreated int `json:"edges_created"` - ElapsedMS int64 `json:"elapsed_ms"` - PostprocessPolicy string `json:"postprocess_policy"` - PolicySource string `json:"policy_source"` - FailedSteps []string `json:"failed_steps"` - SkippedSteps []string `json:"skipped_steps"` + Status string `json:"status"` + FilesParsed int `json:"files_parsed"` + NodesCreated int `json:"nodes_created"` + EdgesCreated int `json:"edges_created"` + ElapsedMS int64 `json:"elapsed_ms"` + FailedSteps []string `json:"failed_steps"` + SkippedSteps []string `json:"skipped_steps"` } // @intent serialize run_postprocess results with a fixed JSON schema without changing the wire format. type runPostprocessResponse struct { - Status string `json:"status"` - FlowsCount int `json:"flows_count"` - CommunitiesCount int `json:"communities_count"` - FTSIndexed int `json:"fts_indexed"` - PostprocessPolicy string `json:"postprocess_policy"` - PolicySource string `json:"policy_source"` - FailedSteps []string `json:"failed_steps"` - SkippedSteps []string `json:"skipped_steps"` + Status string `json:"status"` + FlowsCount int `json:"flows_count"` + CommunitiesCount int `json:"communities_count"` + FTSIndexed int `json:"fts_indexed"` + FailedSteps []string `json:"failed_steps"` + SkippedSteps []string `json:"skipped_steps"` } // @intent apply per-request parse limits without mutating the shared handler dependency configuration. @@ -143,36 +138,14 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR fullRebuild := request.GetBool("full_rebuild", true) postprocess := request.GetString("postprocess", "full") - postprocessPolicy := request.GetString("postprocess_policy", "") - policySource := postprocesspolicy.SourceExplicit - if postprocessPolicy == "" { - policySource = postprocesspolicy.SourceAuto - } includePaths := request.GetStringSlice("include_paths", nil) replace := request.GetBool("replace", true) - if postprocessPolicy != "" && postprocessPolicy != postprocesspolicy.PolicyDegraded && postprocessPolicy != postprocesspolicy.PolicyFailClosed { - return mcp.NewToolResultError("postprocess_policy must be degraded or fail_closed"), nil - } if postprocess != "full" && postprocess != "minimal" && postprocess != "none" { return mcp.NewToolResultError("postprocess must be full, minimal, or none"), nil } - if h.deps.PostprocessPolicy != nil { - resolvedPolicy, resolvedSource, err := h.deps.PostprocessPolicy.Resolve(ctx, postprocesspolicy.DecisionInput{ - Tool: postprocesspolicy.ToolBuildOrUpdateGraph, - ExplicitPolicy: postprocessPolicy, - }) - if err != nil { - return nil, err - } - postprocessPolicy = resolvedPolicy - policySource = resolvedSource - } else if postprocessPolicy == "" { - postprocessPolicy = postprocesspolicy.PolicyDegraded - } - failClosed := postprocessPolicy == postprocesspolicy.PolicyFailClosed && postprocess != "none" - log.Info("build_or_update_graph called", "path", dirPath, "full_rebuild", fullRebuild, "postprocess", postprocess, "postprocess_policy", postprocessPolicy) + log.Info("build_or_update_graph called", "path", dirPath, "full_rebuild", fullRebuild, "postprocess", postprocess) validatedPath, err := h.validateAnalysisPath(dirPath) if err != nil { @@ -222,16 +195,10 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR // Postprocessing var failedSteps []string var skippedSteps []string - var failClosedErr error switch postprocess { case "full": if h.deps.FlowBuilder != nil { if _, err := h.deps.FlowBuilder.Rebuild(ctx, flowspkg.Config{}); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "flows") - break - } log.WarnContext(ctx, "flow rebuild failed", append(obs.TraceLogArgs(ctx), trace.SlogError(err))...) failedSteps = append(failedSteps, "flows") } @@ -241,19 +208,9 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR // search rebuild if h.deps.SearchBackend != nil && h.deps.DB != nil { if _, err := h.refreshSearchDocuments(ctx); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "search_documents") - break - } log.WarnContext(ctx, "search document refresh failed", append(obs.TraceLogArgs(ctx), trace.SlogError(err))...) failedSteps = append(failedSteps, "search_documents") } else if err := h.deps.SearchBackend.Rebuild(ctx, h.deps.DB); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "fts") - break - } log.WarnContext(ctx, "search rebuild failed", append(obs.TraceLogArgs(ctx), trace.SlogError(err))...) failedSteps = append(failedSteps, "fts") } @@ -261,23 +218,13 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR skippedSteps = appendUniqueStrings(skippedSteps, "search_documents", "fts") } case "minimal": - skippedSteps = appendUniqueStrings(skippedSteps, "communities", "flows") + skippedSteps = appendUniqueStrings(skippedSteps, "flows") // search only rebuild if h.deps.SearchBackend != nil && h.deps.DB != nil { if _, err := h.refreshSearchDocuments(ctx); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "search_documents") - break - } log.WarnContext(ctx, "search document refresh failed", append(obs.TraceLogArgs(ctx), trace.SlogError(err))...) failedSteps = append(failedSteps, "search_documents") } else if err := h.deps.SearchBackend.Rebuild(ctx, h.deps.DB); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "fts") - break - } log.WarnContext(ctx, "search rebuild failed", append(obs.TraceLogArgs(ctx), trace.SlogError(err))...) failedSteps = append(failedSteps, "fts") } @@ -286,7 +233,7 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR } case "none": // skip - skippedSteps = appendUniqueStrings(skippedSteps, "communities", "flows", "search_documents", "fts") + skippedSteps = appendUniqueStrings(skippedSteps, "flows", "search_documents", "fts") } elapsed := time.Since(start).Milliseconds() @@ -296,30 +243,13 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR } result := buildOrUpdateGraphResponse{ - Status: status, - FilesParsed: fileCount, - NodesCreated: nodeCount, - EdgesCreated: edgeCount, - ElapsedMS: elapsed, - PostprocessPolicy: postprocessPolicy, - PolicySource: string(policySource), - FailedSteps: failedSteps, - SkippedSteps: skippedSteps, - } - if h.deps.PostprocessPolicy != nil { - if err := h.deps.PostprocessPolicy.RecordRun(ctx, postprocesspolicy.RunRecord{ - Tool: postprocesspolicy.ToolBuildOrUpdateGraph, - Policy: postprocessPolicy, - Source: policySource, - Status: status, - FailedSteps: failedSteps, - SkippedSteps: skippedSteps, - }); err != nil { - return nil, err - } - } - if failClosedErr != nil { - return mcp.NewToolResultError(failClosedErr.Error()), nil + Status: status, + FilesParsed: fileCount, + NodesCreated: nodeCount, + EdgesCreated: edgeCount, + ElapsedMS: elapsed, + FailedSteps: failedSteps, + SkippedSteps: skippedSteps, } jsonStr, err := marshalJSON(result) if err != nil { @@ -343,48 +273,19 @@ func (h *handlers) runPostprocess(ctx context.Context, request mcp.CallToolReque doFlows := request.GetBool("flows", true) doFTS := request.GetBool("fts", true) - postprocessPolicy := request.GetString("postprocess_policy", "") - policySource := postprocesspolicy.SourceExplicit - if postprocessPolicy == "" { - policySource = postprocesspolicy.SourceAuto - } - if postprocessPolicy != "" && postprocessPolicy != postprocesspolicy.PolicyDegraded && postprocessPolicy != postprocesspolicy.PolicyFailClosed { - return mcp.NewToolResultError("postprocess_policy must be degraded or fail_closed"), nil - } - if h.deps.PostprocessPolicy != nil { - resolvedPolicy, resolvedSource, err := h.deps.PostprocessPolicy.Resolve(ctx, postprocesspolicy.DecisionInput{ - Tool: postprocesspolicy.ToolRunPostprocess, - ExplicitPolicy: postprocessPolicy, - }) - if err != nil { - return nil, err - } - postprocessPolicy = resolvedPolicy - policySource = resolvedSource - } else if postprocessPolicy == "" { - postprocessPolicy = postprocesspolicy.PolicyDegraded - } - failClosed := postprocessPolicy == postprocesspolicy.PolicyFailClosed log.Info("run_postprocess called", "flows", doFlows, "fts", doFTS) var flowsCount, communitiesCount, ftsIndexed int var failedSteps []string skippedSteps := []string{} - var failClosedErr error if doFlows { if h.deps.FlowBuilder != nil { stats, err := h.deps.FlowBuilder.Rebuild(ctx, flowspkg.Config{}) if err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "flows") - } - if failClosedErr == nil { - log.Warn("flow rebuild failed", trace.SlogError(err)) - failedSteps = append(failedSteps, "flows") - } + log.Warn("flow rebuild failed", trace.SlogError(err)) + failedSteps = append(failedSteps, "flows") } else { flowsCount = len(stats) } @@ -398,21 +299,11 @@ func (h *handlers) runPostprocess(ctx context.Context, request mcp.CallToolReque if doFTS { if h.deps.SearchBackend != nil && h.deps.DB != nil { if _, err := h.refreshSearchDocuments(ctx); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "search_documents") - } else { - log.Warn("search document refresh failed", trace.SlogError(err)) - failedSteps = append(failedSteps, "search_documents") - } + log.Warn("search document refresh failed", trace.SlogError(err)) + failedSteps = append(failedSteps, "search_documents") } else if err := h.deps.SearchBackend.Rebuild(ctx, h.deps.DB); err != nil { - if failClosed { - failClosedErr = err - failedSteps = append(failedSteps, "fts") - } else { - log.Warn("search rebuild failed", trace.SlogError(err)) - failedSteps = append(failedSteps, "fts") - } + log.Warn("search rebuild failed", trace.SlogError(err)) + failedSteps = append(failedSteps, "fts") } else { ftsIndexed = 1 // at least one rebuild happened } @@ -429,29 +320,12 @@ func (h *handlers) runPostprocess(ctx context.Context, request mcp.CallToolReque } result := runPostprocessResponse{ - Status: status, - FlowsCount: flowsCount, - CommunitiesCount: communitiesCount, - FTSIndexed: ftsIndexed, - PostprocessPolicy: postprocessPolicy, - PolicySource: string(policySource), - FailedSteps: failedSteps, - SkippedSteps: skippedSteps, - } - if h.deps.PostprocessPolicy != nil { - if err := h.deps.PostprocessPolicy.RecordRun(ctx, postprocesspolicy.RunRecord{ - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocessPolicy, - Source: policySource, - Status: status, - FailedSteps: failedSteps, - SkippedSteps: skippedSteps, - }); err != nil { - return nil, err - } - } - if failClosedErr != nil { - return mcp.NewToolResultError(failClosedErr.Error()), nil + Status: status, + FlowsCount: flowsCount, + CommunitiesCount: communitiesCount, + FTSIndexed: ftsIndexed, + FailedSteps: failedSteps, + SkippedSteps: skippedSteps, } jsonStr, err := marshalJSON(result) if err != nil { diff --git a/internal/mcp/handler_postprocess.go b/internal/mcp/handler_postprocess.go deleted file mode 100644 index 3fb9e83..0000000 --- a/internal/mcp/handler_postprocess.go +++ /dev/null @@ -1,74 +0,0 @@ -// @index MCP handlers for inspecting and resetting automatic postprocess policy state. -package mcp - -import ( - "context" - - "github.com/mark3labs/mcp-go/mcp" - - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" -) - -// resetPostprocessPolicyResponse is the typed wire payload for resetPostprocessPolicy. -// @intent preserve a stable confirmation envelope after resetting one postprocess policy tool. -type resetPostprocessPolicyResponse struct { - Status string `json:"status"` - Tool string `json:"tool"` - Reset bool `json:"reset"` -} - -// getPostprocessPolicy returns the recorded postprocess policy summary for a namespace and tool. -// @intent expose automatic fail-open versus fail-closed decisions so operators can diagnose degraded postprocess behavior. -// @param request optional tool filter (build_or_update_graph or run_postprocess) and recent_limit for failure history depth. -// @return JSON-encoded StatusSummary with current decision, fail-closed entries, and recent failures. -// @requires deps.PostprocessPolicy is configured. -// @see mcp.handlers.resetPostprocessPolicy -// @see policy.StatusSummary -func (h *handlers) getPostprocessPolicy(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - ctx = h.applyNamespace(ctx, request) - if h.deps.PostprocessPolicy == nil { - return mcp.NewToolResultError("postprocess policy engine not configured"), nil - } - tool := request.GetString("tool", "") - if tool != "" && !postprocesspolicy.ValidTool(tool) { - return mcp.NewToolResultError("tool must be build_or_update_graph or run_postprocess"), nil - } - recentLimit := request.GetInt("recent_limit", postprocesspolicy.DefaultStatusLimit) - if err := validatePositiveLimit(recentLimit); err != nil { - return finalizeToolResult("", err) - } - summary, err := h.deps.PostprocessPolicy.Status(ctx, postprocesspolicy.StatusOptions{ - Namespace: requestNamespace(request), - Tool: tool, - RecentLimit: recentLimit, - }) - if err != nil { - return nil, err - } - result, err := marshalJSON(summary) - return finalizeToolResult(result, err) -} - -// resetPostprocessPolicy clears the stored failure streak for a postprocess tool. -// @intent let operators recover from fail-closed state after they have fixed the underlying issue. -// @param request requires tool name (build_or_update_graph or run_postprocess) to reset. -// @requires deps.PostprocessPolicy is configured and tool name is valid. -// @ensures next policy resolve treats the tool as if no recent failures occurred. -// @sideEffect writes a reset marker run record into the postprocess policy store. -// @mutates postprocess policy persisted state for the targeted tool. -// @see mcp.handlers.getPostprocessPolicy -func (h *handlers) resetPostprocessPolicy(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - ctx = h.applyNamespace(ctx, request) - if h.deps.PostprocessPolicy == nil { - return mcp.NewToolResultError("postprocess policy engine not configured"), nil - } - tool := request.GetString("tool", "") - if !postprocesspolicy.ValidTool(tool) { - return mcp.NewToolResultError("tool must be build_or_update_graph or run_postprocess"), nil - } - if err := h.deps.PostprocessPolicy.Reset(ctx, tool); err != nil { - return nil, err - } - result, err := marshalJSON(resetPostprocessPolicyResponse{Status: "ok", Tool: tool, Reset: true}) - return finalizeToolResult(result, err) -} diff --git a/internal/mcp/handlers_test.go b/internal/mcp/handlers_test.go index d22aa79..0c7389f 100644 --- a/internal/mcp/handlers_test.go +++ b/internal/mcp/handlers_test.go @@ -24,7 +24,6 @@ import ( "github.com/tae2089/code-context-graph/internal/ctxns" "github.com/tae2089/code-context-graph/internal/model" "github.com/tae2089/code-context-graph/internal/obs" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" ) func TestMarshalJSON(t *testing.T) { @@ -1259,28 +1258,6 @@ func Run() {} } } -func TestBuildOrUpdateGraph_RejectsInvalidPostprocessPolicy(t *testing.T) { - deps := setupTestDeps(t) - dir := t.TempDir() - writeGoFile(t, dir, "svc.go", `package svc - -func Run() {} -`) - - result := callTool(t, deps, "build_or_update_graph", map[string]any{ - "path": dir, - "full_rebuild": true, - "postprocess": "none", - "postprocess_policy": "strict", - }) - if !result.IsError { - t.Fatalf("expected tool error for invalid postprocess_policy, got: %s", getTextContent(result)) - } - if !strings.Contains(getTextContent(result), "postprocess_policy must be degraded or fail_closed") { - t.Fatalf("unexpected error: %s", getTextContent(result)) - } -} - func containsString(values []any, target string) bool { for _, v := range values { if s, ok := v.(string); ok && s == target { @@ -1457,113 +1434,6 @@ func TestRunPostprocess_EmptyFailedStepsIsNullJSON(t *testing.T) { } } -func TestRunPostprocess_PassesExplicitPolicyToResolverAndRecordsRun(t *testing.T) { - deps := setupTestDeps(t) - stub := &stubPostprocessPolicy{resolvedPolicy: "degraded", resolvedSource: "explicit"} - deps.PostprocessPolicy = stub - deps.SearchBackend = &failSearchBackend{err: errors.New("fts rebuild boom")} - - result := callTool(t, deps, "run_postprocess", map[string]any{ - "communities": false, - "fts": true, - "flows": false, - "postprocess_policy": "degraded", - }) - if result.IsError { - t.Fatalf("expected explicit degraded resolver result not to error, got: %s", getTextContent(result)) - } - if got := len(stub.resolvedInputs); got != 1 { - t.Fatalf("resolve calls = %d, want 1", got) - } - if stub.resolvedInputs[0].ExplicitPolicy != "degraded" { - t.Fatalf("explicit policy = %q, want degraded", stub.resolvedInputs[0].ExplicitPolicy) - } - if got := len(stub.recordedRuns); got != 1 { - t.Fatalf("recorded runs = %d, want 1", got) - } - if stub.recordedRuns[0].Tool != "run_postprocess" { - t.Fatalf("recorded tool = %q, want run_postprocess", stub.recordedRuns[0].Tool) - } - if stub.recordedRuns[0].Policy != "degraded" { - t.Fatalf("recorded policy = %q, want degraded", stub.recordedRuns[0].Policy) - } - - var resp map[string]any - if err := json.Unmarshal([]byte(getTextContent(result)), &resp); err != nil { - t.Fatalf("expected JSON, got: %s", getTextContent(result)) - } - if resp["postprocess_policy"] != "degraded" { - t.Fatalf("response postprocess_policy = %v, want degraded", resp["postprocess_policy"]) - } - if resp["policy_source"] != "explicit" { - t.Fatalf("response policy_source = %v, want explicit", resp["policy_source"]) - } -} - -func TestGetPostprocessPolicy_UsesPolicyStatusSummary(t *testing.T) { - deps := setupTestDeps(t) - deps.PostprocessPolicy = &stubPostprocessPolicy{ - statusSummary: &postprocesspolicy.StatusSummary{ - Status: postprocesspolicy.StatusDegraded, - FailClosed: []postprocesspolicy.StateSnapshot{{ - Namespace: ctxns.DefaultNamespace, - Tool: postprocesspolicy.ToolRunPostprocess, - Policy: postprocesspolicy.PolicyFailClosed, - ConsecutiveFailures: 3, - }}, - }, - } - - result := callTool(t, deps, "get_postprocess_policy", map[string]any{"tool": "run_postprocess", "recent_limit": 3}) - if result.IsError { - t.Fatalf("get_postprocess_policy error: %s", getTextContent(result)) - } - stub := deps.PostprocessPolicy.(*stubPostprocessPolicy) - if len(stub.statusInputs) != 1 { - t.Fatalf("status inputs = %d, want 1", len(stub.statusInputs)) - } - if stub.statusInputs[0].Tool != postprocesspolicy.ToolRunPostprocess { - t.Fatalf("status tool = %q, want run_postprocess", stub.statusInputs[0].Tool) - } - var resp map[string]any - if err := json.Unmarshal([]byte(getTextContent(result)), &resp); err != nil { - t.Fatalf("expected JSON, got: %s", getTextContent(result)) - } - if resp["status"] != "degraded" { - t.Fatalf("status = %v, want degraded", resp["status"]) - } -} - -func TestResetPostprocessPolicy_RecordsResetForTool(t *testing.T) { - deps := setupTestDeps(t) - deps.PostprocessPolicy = &stubPostprocessPolicy{} - - result := callTool(t, deps, "reset_postprocess_policy", map[string]any{"tool": "run_postprocess"}) - if result.IsError { - t.Fatalf("reset_postprocess_policy error: %s", getTextContent(result)) - } - stub := deps.PostprocessPolicy.(*stubPostprocessPolicy) - if len(stub.resetTools) != 1 { - t.Fatalf("reset tools = %d, want 1", len(stub.resetTools)) - } - if stub.resetTools[0] != postprocesspolicy.ToolRunPostprocess { - t.Fatalf("reset tool = %q, want run_postprocess", stub.resetTools[0]) - } -} - -func TestResetPostprocessPolicy_RejectsInvalidTool(t *testing.T) { - deps := setupTestDeps(t) - deps.PostprocessPolicy = &stubPostprocessPolicy{} - - result := callTool(t, deps, "reset_postprocess_policy", map[string]any{"tool": "other"}) - if !result.IsError { - t.Fatalf("expected invalid tool to fail, got: %s", getTextContent(result)) - } - if !strings.Contains(getTextContent(result), "tool must be build_or_update_graph or run_postprocess") { - t.Fatalf("unexpected error: %s", getTextContent(result)) - } -} - // ============================================================ // 11.3 query_graph // ============================================================ diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 3cd47d1..21aec57 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -18,8 +18,6 @@ func TestMCPServer_ListTools(t *testing.T) { expected := []string{ "parse_project", - "get_postprocess_policy", - "reset_postprocess_policy", "get_node", "get_impact_radius", "search", @@ -72,8 +70,8 @@ func TestMCPServer_ListTools_Count(t *testing.T) { srv := NewServer(deps) tools := srv.ListTools() - if len(tools) != 26 { - t.Fatalf("expected 26 tools, got %d", len(tools)) + if len(tools) != 24 { + t.Fatalf("expected 24 tools, got %d", len(tools)) } } @@ -159,8 +157,6 @@ func TestMCPServer_ToolRequiredFlags(t *testing.T) { expected := map[string][]string{ "parse_project": {"path"}, - "get_postprocess_policy": nil, - "reset_postprocess_policy": {"tool"}, "get_node": {"qualified_name"}, "get_impact_radius": {"qualified_name"}, "search": {"query"}, diff --git a/internal/mcp/testhelpers_test.go b/internal/mcp/testhelpers_test.go index 9ed7264..afea952 100644 --- a/internal/mcp/testhelpers_test.go +++ b/internal/mcp/testhelpers_test.go @@ -22,7 +22,6 @@ import ( "github.com/tae2089/code-context-graph/internal/ctxns" "github.com/tae2089/code-context-graph/internal/model" "github.com/tae2089/code-context-graph/internal/parse/treesitter" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" "github.com/tae2089/code-context-graph/internal/store/gormstore" "github.com/tae2089/code-context-graph/internal/store/search" ) @@ -170,11 +169,7 @@ func setupTestDeps(t *testing.T) *Deps { FlowTracer: flows.New(st), FlowBuilder: flows.NewBuilder(db, st), Logger: logger, - PostprocessPolicy: &stubPostprocessPolicy{ - resolvedPolicy: postprocesspolicy.PolicyDegraded, - resolvedSource: postprocesspolicy.SourceAuto, - }, - RepoRoot: os.TempDir(), + RepoRoot: os.TempDir(), } } @@ -212,11 +207,7 @@ func setupTestDepsMinimal(t *testing.T) *Deps { ImpactAnalyzer: impact.New(st), FlowTracer: flows.New(st), Logger: logger, - PostprocessPolicy: &stubPostprocessPolicy{ - resolvedPolicy: postprocesspolicy.PolicyDegraded, - resolvedSource: postprocesspolicy.SourceAuto, - }, - RepoRoot: os.TempDir(), + RepoRoot: os.TempDir(), // Note: QueryService, LargefuncAnalyzer, DeadcodeAnalyzer, CouplingAnalyzer, // CoverageAnalyzer, FlowBuilder, and Incremental are intentionally nil } @@ -251,88 +242,6 @@ func setupGraphOnlyTestDeps(t *testing.T) *Deps { } } -type stubPostprocessPolicy struct { - resolvedPolicy string - resolvedSource string - resolveErr error - recordErr error - statusErr error - resetErr error - statusSummary *postprocesspolicy.StatusSummary - resolvedInputs []postprocesspolicy.DecisionInput - recordedRuns []postprocesspolicy.RunRecord - statusInputs []postprocesspolicy.StatusOptions - resetTools []string -} - -func (s *stubPostprocessPolicy) Resolve(ctx context.Context, input postprocesspolicy.DecisionInput) (string, string, error) { - s.resolvedInputs = append(s.resolvedInputs, input) - if s.resolveErr != nil { - return "", "", s.resolveErr - } - // Respect explicit policy if provided - if input.ExplicitPolicy != "" { - return input.ExplicitPolicy, postprocesspolicy.SourceExplicit, nil - } - return s.resolvedPolicy, s.resolvedSource, nil -} - -func (s *stubPostprocessPolicy) RecordRun(ctx context.Context, record postprocesspolicy.RunRecord) error { - s.recordedRuns = append(s.recordedRuns, record) - return s.recordErr -} - -func (s *stubPostprocessPolicy) Status(ctx context.Context, opts postprocesspolicy.StatusOptions) (*postprocesspolicy.StatusSummary, error) { - s.statusInputs = append(s.statusInputs, opts) - if s.statusErr != nil { - return nil, s.statusErr - } - if s.statusSummary == nil { - return &postprocesspolicy.StatusSummary{Status: postprocesspolicy.StatusOK}, nil - } - return s.statusSummary, nil -} - -func (s *stubPostprocessPolicy) Reset(ctx context.Context, tool string) error { - s.resetTools = append(s.resetTools, tool) - return s.resetErr -} - -type realPostprocessPolicy struct { - engine *postprocesspolicy.Engine - store *postprocesspolicy.Store -} - -func newRealPostprocessPolicy(db *gorm.DB) *realPostprocessPolicy { - return &realPostprocessPolicy{ - engine: &postprocesspolicy.Engine{}, - store: postprocesspolicy.NewStore(db), - } -} - -func (p *realPostprocessPolicy) Resolve(ctx context.Context, input postprocesspolicy.DecisionInput) (string, string, error) { - return p.engine.Resolve(ctx, p.store, input) -} - -func (p *realPostprocessPolicy) RecordRun(ctx context.Context, record postprocesspolicy.RunRecord) error { - return p.store.RecordRun(ctx, record) -} - -func (p *realPostprocessPolicy) Status(ctx context.Context, opts postprocesspolicy.StatusOptions) (*postprocesspolicy.StatusSummary, error) { - return p.store.Status(ctx, opts) -} - -func (p *realPostprocessPolicy) Reset(ctx context.Context, tool string) error { - return p.store.Reset(ctx, tool) -} - -func setupTestDepsWithRealPostprocessPolicy(t *testing.T) *Deps { - t.Helper() - deps := setupTestDeps(t) - deps.PostprocessPolicy = newRealPostprocessPolicy(deps.DB) - return deps -} - func callToolWithNamespace(t *testing.T, deps *Deps, namespace, toolName string, args map[string]any) *mcp.CallToolResult { t.Helper() srv := NewServer(deps) diff --git a/internal/mcp/tools_parse.go b/internal/mcp/tools_parse.go index 8f039cc..decc471 100644 --- a/internal/mcp/tools_parse.go +++ b/internal/mcp/tools_parse.go @@ -26,7 +26,6 @@ func parseTools(h *handlers) []server.ServerTool { mcp.WithString("path", mcp.Description("Project directory path to parse"), mcp.Required()), mcp.WithBoolean("full_rebuild", mcp.Description("If true, do a full rebuild; if false, use incremental sync")), mcp.WithString("postprocess", mcp.Description("Postprocessing mode: full, minimal, or none (default: full)")), - mcp.WithString("postprocess_policy", mcp.Description("Postprocessing failure policy: degraded or fail_closed (default: auto -> degraded, escalates to fail_closed after repeated failures)")), mcp.WithArray("include_paths", mcp.Description("Only include specific sub-paths (e.g. [\"src/api\", \"src/auth\"])"), mcp.WithStringItems()), mcp.WithBoolean("replace", mcp.Description("When true (default), incremental include_paths replaces prior namespace graph state outside the included scope; when false, preserves out-of-scope files")), mcp.WithNumber("max_file_bytes", mcp.Description("Maximum bytes allowed per parsed source file; overrides server default when set")), @@ -36,8 +35,7 @@ func parseTools(h *handlers) []server.ServerTool { }, { Tool: mcp.NewTool("run_postprocess", withNamespaceParam( - mcp.WithDescription("Run postprocessing steps independently: rebuild stored flows, communities, and/or full-text search indexing"), - mcp.WithString("postprocess_policy", mcp.Description("Postprocessing failure policy: degraded or fail_closed (default: auto -> degraded, escalates to fail_closed after repeated failures)")), + mcp.WithDescription("Run postprocessing steps independently: rebuild stored flows and/or full-text search indexing"), mcp.WithBoolean("flows", mcp.Description("Rebuild persisted stored flows when FlowBuilder is configured; otherwise report flows as skipped (default: true)")), mcp.WithBoolean("communities", mcp.Description("Rebuild community detection (default: true)")), mcp.WithBoolean("fts", mcp.Description("Rebuild full-text search index (default: true)")), diff --git a/internal/mcp/tools_postprocess.go b/internal/mcp/tools_postprocess.go deleted file mode 100644 index 975af6d..0000000 --- a/internal/mcp/tools_postprocess.go +++ /dev/null @@ -1,29 +0,0 @@ -// @index MCP tool registration for automatic postprocess policy control. -package mcp - -import ( - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -// postprocessTools registers policy inspection and reset tools for postprocess automation. -// @intent make postprocess recovery controls available without mixing them into unrelated tool groups. -func postprocessTools(h *handlers) []server.ServerTool { - return []server.ServerTool{ - { - Tool: mcp.NewTool("get_postprocess_policy", withNamespaceParam( - mcp.WithDescription("Inspect automatic postprocess policy state, fail_closed entries, and recent failures for the current namespace or across namespaces"), - mcp.WithString("tool", mcp.Description("Optional tool filter: build_or_update_graph or run_postprocess")), - mcp.WithNumber("recent_limit", mcp.Description("Maximum recent failures returned (default: 5)"), mcp.DefaultNumber(5)), - )...), - Handler: h.getPostprocessPolicy, - }, - { - Tool: mcp.NewTool("reset_postprocess_policy", withNamespaceParam( - mcp.WithDescription("Reset automatic postprocess failure streak for a tool by recording a reset marker run in the current namespace"), - mcp.WithString("tool", mcp.Description("Tool to reset: build_or_update_graph or run_postprocess"), mcp.Required()), - )...), - Handler: h.resetPostprocessPolicy, - }, - } -} diff --git a/internal/mcp/tools_register.go b/internal/mcp/tools_register.go index ebdb489..e20bf98 100644 --- a/internal/mcp/tools_register.go +++ b/internal/mcp/tools_register.go @@ -10,7 +10,6 @@ import "github.com/mark3labs/mcp-go/server" func registerTools(srv *server.MCPServer, h *handlers) { var tools []server.ServerTool tools = append(tools, parseTools(h)...) - tools = append(tools, postprocessTools(h)...) tools = append(tools, queryTools(h)...) tools = append(tools, analysisTools(h)...) tools = append(tools, graphTools(h)...) diff --git a/internal/mcpruntime/runtime.go b/internal/mcpruntime/runtime.go index b5b8c7b..0151867 100644 --- a/internal/mcpruntime/runtime.go +++ b/internal/mcpruntime/runtime.go @@ -11,7 +11,6 @@ import ( mcpgo "github.com/mark3labs/mcp-go/server" "github.com/tae2089/trace" - "gorm.io/gorm" "github.com/tae2089/code-context-graph/internal/analysis/changes" "github.com/tae2089/code-context-graph/internal/analysis/flows" @@ -20,7 +19,6 @@ import ( "github.com/tae2089/code-context-graph/internal/core" "github.com/tae2089/code-context-graph/internal/mcp" ccgobs "github.com/tae2089/code-context-graph/internal/obs" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" ) // Options controls shared MCP runtime setup independent of transport. @@ -41,17 +39,16 @@ type Options struct { // Instance is a fully assembled MCP server plus runtime resources. // @intent share MCP server construction while keeping stdio and HTTP transports in separate packages. type Instance struct { - Server *mcpgo.MCPServer - Cache *mcp.Cache - Deps *mcp.Deps - PostprocessSummary func(context.Context) (*postprocesspolicy.StatusSummary, error) + Server *mcpgo.MCPServer + Cache *mcp.Cache + Deps *mcp.Deps logger *slog.Logger shutdown func(context.Context) error close sync.Once } -// New assembles MCP handlers, cache, telemetry, and postprocess policy. +// New assembles MCP handlers, cache, and telemetry. // @intent centralize common MCP dependency wiring without linking webhook/HTTP code into the local CLI binary. // @sideEffect initializes telemetry and optional in-memory cache. func New(rt *core.Runtime, opts Options) (*Instance, error) { @@ -90,7 +87,6 @@ func New(rt *core.Runtime, opts Options) (*Instance, error) { QueryService: query.New(rt.DB), FlowBuilder: flows.NewBuilder(rt.DB, rt.Store), Incremental: rt.Syncer, - PostprocessPolicy: NewPostprocessPolicy(rt.DB), Logger: rt.Logger, Cache: cache, RagIndexDir: opts.RagIndexDir, @@ -108,12 +104,6 @@ func New(rt *core.Runtime, opts Options) (*Instance, error) { logger: rt.Logger, shutdown: tel.Shutdown, } - inst.PostprocessSummary = func(ctx context.Context) (*postprocesspolicy.StatusSummary, error) { - if mcpDeps.PostprocessPolicy == nil { - return nil, nil - } - return mcpDeps.PostprocessPolicy.Status(ctx, postprocesspolicy.StatusOptions{RecentLimit: postprocesspolicy.DefaultStatusLimit}) - } return inst, nil } @@ -172,48 +162,3 @@ func FlushQueryCache(cache *mcp.Cache) { cache.Flush() } } - -// MCPPostprocessPolicy manages post-processing policies for the MCP runtime. -// @intent MCP 실행 경로가 후처리 정책 결정을 공통 래퍼로 호출하게 한다. -type MCPPostprocessPolicy struct { - engine *postprocesspolicy.Engine - store *postprocesspolicy.Store -} - -// NewPostprocessPolicy creates a new MCP post-processing policy wrapper. -// @intent MCP 실행 경로에서 후처리 정책 엔진과 저장소를 함께 묶어 제공한다. -func NewPostprocessPolicy(db *gorm.DB) *MCPPostprocessPolicy { - if db == nil { - return nil - } - return &MCPPostprocessPolicy{ - engine: &postprocesspolicy.Engine{}, - store: postprocesspolicy.NewStore(db), - } -} - -// Resolve decides the policy for a given tool and input. -// @intent 요청된 후처리 도구에 적용할 정책과 출처를 계산한다. -func (p *MCPPostprocessPolicy) Resolve(ctx context.Context, input postprocesspolicy.DecisionInput) (string, string, error) { - return p.engine.Resolve(ctx, p.store, input) -} - -// RecordRun logs the results of a post-processing run. -// @intent 후처리 실행 결과를 정책 저장소에 기록해 후속 판단에 반영한다. -// @sideEffect ccg_postprocess_run_logs 상태를 갱신한다. -func (p *MCPPostprocessPolicy) RecordRun(ctx context.Context, record postprocesspolicy.RunRecord) error { - return p.store.RecordRun(ctx, record) -} - -// Status returns the current status summary of post-processing. -// @intent 운영 상태 엔드포인트가 후처리 건강 상태를 요약해서 볼 수 있게 한다. -func (p *MCPPostprocessPolicy) Status(ctx context.Context, opts postprocesspolicy.StatusOptions) (*postprocesspolicy.StatusSummary, error) { - return p.store.Status(ctx, opts) -} - -// Reset clears the state of a specific post-processing tool. -// @intent 실패 누적 상태를 초기화해 특정 후처리 도구를 다시 정상 정책으로 돌린다. -// @sideEffect 해당 도구의 정책 상태를 재설정한다. -func (p *MCPPostprocessPolicy) Reset(ctx context.Context, tool string) error { - return p.store.Reset(ctx, tool) -} diff --git a/internal/migrationfs/postgres/000007_drop_postprocess_policy.down.sql b/internal/migrationfs/postgres/000007_drop_postprocess_policy.down.sql new file mode 100644 index 0000000..7ee5cf4 --- /dev/null +++ b/internal/migrationfs/postgres/000007_drop_postprocess_policy.down.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS ccg_postprocess_policy_state ( + namespace varchar(256) NOT NULL, + tool varchar(64) NOT NULL, + policy varchar(32) NOT NULL, + updated_at timestamptz NOT NULL, + PRIMARY KEY (namespace, tool) +); + +CREATE TABLE IF NOT EXISTS ccg_postprocess_run_logs ( + id bigserial PRIMARY KEY, + namespace varchar(256) NOT NULL, + tool varchar(64) NOT NULL, + policy varchar(32) NOT NULL, + source varchar(16) NOT NULL, + status varchar(16) NOT NULL, + failed_steps jsonb NOT NULL DEFAULT '[]'::jsonb, + skipped_steps jsonb NOT NULL DEFAULT '[]'::jsonb, + created_at timestamptz NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_pp_log_ns_tool_time +ON ccg_postprocess_run_logs(namespace, tool, created_at DESC); diff --git a/internal/migrationfs/postgres/000007_drop_postprocess_policy.up.sql b/internal/migrationfs/postgres/000007_drop_postprocess_policy.up.sql new file mode 100644 index 0000000..9314e66 --- /dev/null +++ b/internal/migrationfs/postgres/000007_drop_postprocess_policy.up.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS idx_pp_log_ns_tool_time; +DROP TABLE IF EXISTS ccg_postprocess_run_logs; +DROP TABLE IF EXISTS ccg_postprocess_policy_state; diff --git a/internal/migrationfs/sqlite/000007_drop_postprocess_policy.down.sql b/internal/migrationfs/sqlite/000007_drop_postprocess_policy.down.sql new file mode 100644 index 0000000..8f9db90 --- /dev/null +++ b/internal/migrationfs/sqlite/000007_drop_postprocess_policy.down.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS ccg_postprocess_policy_state ( + namespace text NOT NULL, + tool text NOT NULL, + policy text NOT NULL, + updated_at datetime NOT NULL, + PRIMARY KEY (namespace, tool) +); + +CREATE TABLE IF NOT EXISTS ccg_postprocess_run_logs ( + id integer PRIMARY KEY AUTOINCREMENT, + namespace text NOT NULL, + tool text NOT NULL, + policy text NOT NULL, + source text NOT NULL, + status text NOT NULL, + failed_steps text NOT NULL DEFAULT '[]', + skipped_steps text NOT NULL DEFAULT '[]', + created_at datetime NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_pp_log_ns_tool_time +ON ccg_postprocess_run_logs(namespace, tool, created_at DESC); diff --git a/internal/migrationfs/sqlite/000007_drop_postprocess_policy.up.sql b/internal/migrationfs/sqlite/000007_drop_postprocess_policy.up.sql new file mode 100644 index 0000000..9314e66 --- /dev/null +++ b/internal/migrationfs/sqlite/000007_drop_postprocess_policy.up.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS idx_pp_log_ns_tool_time; +DROP TABLE IF EXISTS ccg_postprocess_run_logs; +DROP TABLE IF EXISTS ccg_postprocess_policy_state; diff --git a/internal/model/postprocess_policy.go b/internal/model/postprocess_policy.go deleted file mode 100644 index 612702b..0000000 --- a/internal/model/postprocess_policy.go +++ /dev/null @@ -1,39 +0,0 @@ -// @index GORM models for persisted automatic postprocess policy state. -package model - -import "time" - -// PostprocessPolicyState stores the latest effective postprocess policy per namespace and tool. -// @intent 자동 정책 엔진의 최신 판단 상태를 namespace/tool 단위로 유지한다. -type PostprocessPolicyState struct { - Namespace string `gorm:"primaryKey;size:256"` - Tool string `gorm:"primaryKey;size:64"` - Policy string `gorm:"size:32;not null"` - UpdatedAt time.Time `gorm:"not null"` -} - -// TableName pins PostprocessPolicyState to the shared policy state table name. -// @intent keep migration-managed table names stable across refactors and GORM defaults. -func (PostprocessPolicyState) TableName() string { - return "ccg_postprocess_policy_state" -} - -// PostprocessRunLog appends the effective policy and outcome of each run. -// @intent 자동 정책 엔진의 실행 이력과 결과를 추적해 후속 판단 근거로 사용한다. -type PostprocessRunLog struct { - ID uint `gorm:"primaryKey"` - Namespace string `gorm:"size:256;not null;index:idx_pp_log_ns_tool_time,priority:1"` - Tool string `gorm:"size:64;not null;index:idx_pp_log_ns_tool_time,priority:2"` - Policy string `gorm:"size:32;not null"` - Source string `gorm:"size:16;not null"` - Status string `gorm:"size:16;not null"` - FailedSteps string `gorm:"type:text;not null;default:'[]'"` - SkippedSteps string `gorm:"type:text;not null;default:'[]'"` - CreatedAt time.Time `gorm:"not null;index:idx_pp_log_ns_tool_time,priority:3,sort:desc"` -} - -// TableName pins PostprocessRunLog to the shared postprocess run log table name. -// @intent preserve schema compatibility for policy history queries and migrations. -func (PostprocessRunLog) TableName() string { - return "ccg_postprocess_run_logs" -} diff --git a/internal/postprocess/policy/engine.go b/internal/postprocess/policy/engine.go deleted file mode 100644 index 42db04b..0000000 --- a/internal/postprocess/policy/engine.go +++ /dev/null @@ -1,411 +0,0 @@ -// @index Automatic postprocess policy state, decision logic, and persistence. -package policy - -import ( - "context" - "encoding/json" - "sort" - "time" - - "gorm.io/gorm" - "gorm.io/gorm/clause" - - "github.com/tae2089/code-context-graph/internal/ctxns" - "github.com/tae2089/code-context-graph/internal/model" - "github.com/tae2089/trace" -) - -const ( - PolicyDegraded = "degraded" - PolicyFailClosed = "fail_closed" - - SourceAuto = "auto" - SourceExplicit = "explicit" - SourceReset = "reset" - - StatusOK = "ok" - StatusDegraded = "degraded" - - ToolBuildOrUpdateGraph = "build_or_update_graph" - ToolRunPostprocess = "run_postprocess" - - DefaultRunLogRetention = 200 - DefaultStatusLimit = 5 -) - -// @intent scope policy status queries by namespace, tool, and recent-run history length. -type StatusOptions struct { - Namespace string - Tool string - RecentLimit int -} - -// @intent expose the latest persisted automatic policy state for one namespace and tool. -type StateSnapshot struct { - Namespace string `json:"namespace"` - Tool string `json:"tool"` - Policy string `json:"policy"` - UpdatedAt time.Time `json:"updated_at"` - ConsecutiveFailures int `json:"consecutive_failures"` -} - -// @intent describe one recorded postprocess run for status and failure inspection. -type RunSnapshot struct { - Namespace string `json:"namespace"` - Tool string `json:"tool"` - Policy string `json:"policy"` - Source string `json:"source"` - Status string `json:"status"` - FailedSteps []string `json:"failed_steps"` - SkippedSteps []string `json:"skipped_steps"` - CreatedAt time.Time `json:"created_at"` -} - -// @intent bundle fail-closed state and recent failures into one operator-facing policy summary. -type StatusSummary struct { - Status string `json:"status"` - FailClosed []StateSnapshot `json:"fail_closed,omitempty"` - RecentFailures []RunSnapshot `json:"recent_failures,omitempty"` -} - -// @intent carry the inputs that influence automatic postprocess policy resolution. -type DecisionInput struct { - Tool string - ExplicitPolicy string -} - -// @intent capture the outcome and policy metadata of one postprocess execution. -type RunRecord struct { - Tool string - Policy string - Source string - Status string - FailedSteps []string - SkippedSteps []string - CreatedAt time.Time -} - -// @intent resolve effective postprocess policy from explicit input plus stored failure history. -type Engine struct{} - -// @intent persist and query namespace-scoped postprocess policy state and run history. -type Store struct { - db *gorm.DB - runLogRetention int -} - -// NewStore creates a persistence helper for postprocess policy state and run logs. -// @intent keep policy decisions and failure streaks queryable across build and postprocess executions. -func NewStore(db *gorm.DB) *Store { - return &Store{db: db, runLogRetention: DefaultRunLogRetention} -} - -// Resolve selects the effective postprocess policy for the current namespace and tool. -// @intent default to degraded execution while escalating to fail_closed after repeated recent failures. -func (e *Engine) Resolve(ctx context.Context, store *Store, input DecisionInput) (string, string, error) { - if input.ExplicitPolicy != "" { - return input.ExplicitPolicy, SourceExplicit, nil - } - if store == nil { - return PolicyDegraded, SourceAuto, nil - } - count, err := store.ConsecutiveFailures(ctx, input.Tool, 3) - if err != nil { - return "", "", err - } - if count >= 3 { - return PolicyFailClosed, SourceAuto, nil - } - return PolicyDegraded, SourceAuto, nil -} - -// GetState returns the latest stored postprocess policy for the active namespace and tool. -// @intent expose the current automatic policy decision without scanning historical runs. -func (s *Store) GetState(ctx context.Context, tool string) (*model.PostprocessPolicyState, error) { - var state model.PostprocessPolicyState - ns := ctxns.FromContext(ctx) - err := s.db.WithContext(ctx).Where("namespace = ? AND tool = ?", ns, tool).First(&state).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, trace.Wrap(err, "get postprocess policy state") - } - return &state, nil -} - -// RecordRun appends one postprocess execution result and updates the latest policy snapshot. -// @intent preserve the audit trail needed for failure escalation while keeping a cheap current-state lookup. -// @sideEffect writes a run log row and upserts namespace-scoped policy state. -func (s *Store) RecordRun(ctx context.Context, record RunRecord) error { - ns := ctxns.FromContext(ctx) - createdAt := record.CreatedAt - if createdAt.IsZero() { - createdAt = time.Now().UTC() - } - failedJSON, err := marshalStringSlice(record.FailedSteps) - if err != nil { - return err - } - skippedJSON, err := marshalStringSlice(record.SkippedSteps) - if err != nil { - return err - } - return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - log := &model.PostprocessRunLog{ - Namespace: ns, - Tool: record.Tool, - Policy: record.Policy, - Source: record.Source, - Status: record.Status, - FailedSteps: failedJSON, - SkippedSteps: skippedJSON, - CreatedAt: createdAt, - } - if err := tx.Create(log).Error; err != nil { - return trace.Wrap(err, "create postprocess run log") - } - effectivePolicy := record.Policy - if record.Status == StatusOK { - effectivePolicy = PolicyDegraded - } - state := &model.PostprocessPolicyState{ - Namespace: ns, - Tool: record.Tool, - Policy: effectivePolicy, - UpdatedAt: createdAt, - } - if err := tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "namespace"}, {Name: "tool"}}, - DoUpdates: clause.AssignmentColumns([]string{"policy", "updated_at"}), - }).Create(state).Error; err != nil { - return trace.Wrap(err, "upsert postprocess policy state") - } - if err := s.pruneRunLogs(tx, ns, record.Tool); err != nil { - return err - } - return nil - }) -} - -// Reset records a successful reset marker for the named postprocess tool. -// @intent clear automatic fail_closed escalation after an operator has remediated the underlying issue. -func (s *Store) Reset(ctx context.Context, tool string) error { - if !ValidTool(tool) { - return trace.New("invalid postprocess tool") - } - return s.RecordRun(ctx, RunRecord{ - Tool: tool, - Policy: PolicyDegraded, - Source: SourceReset, - Status: StatusOK, - CreatedAt: time.Now().UTC(), - }) -} - -// Status summarizes fail-closed state and recent failures for the requested scope. -// @intent give operators one status view that explains why automatic postprocess execution is degraded. -func (s *Store) Status(ctx context.Context, opts StatusOptions) (*StatusSummary, error) { - limit := opts.RecentLimit - if limit <= 0 { - limit = DefaultStatusLimit - } - states, err := s.listStates(ctx, opts.Namespace, opts.Tool) - if err != nil { - return nil, err - } - summary := &StatusSummary{Status: StatusOK} - recentFailures := make([]RunSnapshot, 0, limit) - for _, state := range states { - count, err := s.consecutiveFailuresScoped(ctx, state.Namespace, state.Tool, limit) - if err != nil { - return nil, err - } - snapshot := StateSnapshot{ - Namespace: state.Namespace, - Tool: state.Tool, - Policy: state.Policy, - UpdatedAt: state.UpdatedAt, - ConsecutiveFailures: count, - } - if state.Policy == PolicyFailClosed && count > 0 { - summary.FailClosed = append(summary.FailClosed, snapshot) - } - if count == 0 { - continue - } - runs, err := s.listLatestFailedRuns(ctx, state.Namespace, state.Tool, count) - if err != nil { - return nil, err - } - for _, run := range runs { - recentFailures = append(recentFailures, run) - } - } - sort.Slice(summary.FailClosed, func(i, j int) bool { - return summary.FailClosed[i].UpdatedAt.After(summary.FailClosed[j].UpdatedAt) - }) - sort.Slice(recentFailures, func(i, j int) bool { - if recentFailures[i].CreatedAt.Equal(recentFailures[j].CreatedAt) { - if recentFailures[i].Namespace == recentFailures[j].Namespace { - return recentFailures[i].Tool < recentFailures[j].Tool - } - return recentFailures[i].Namespace < recentFailures[j].Namespace - } - return recentFailures[i].CreatedAt.After(recentFailures[j].CreatedAt) - }) - if len(recentFailures) > limit { - recentFailures = recentFailures[:limit] - } - summary.RecentFailures = recentFailures - if len(summary.FailClosed) > 0 || len(summary.RecentFailures) > 0 { - summary.Status = StatusDegraded - } - return summary, nil -} - -// ConsecutiveFailures counts recent non-success runs for the active namespace and tool. -// @intent power escalation decisions without leaking cross-namespace failure history. -func (s *Store) ConsecutiveFailures(ctx context.Context, tool string, limit int) (int, error) { - if limit <= 0 { - return 0, nil - } - ns := ctxns.FromContext(ctx) - return s.consecutiveFailuresScoped(ctx, ns, tool, limit) -} - -// ValidTool reports whether a tool participates in automatic postprocess policy tracking. -// @intent reject arbitrary tool names before they can create inconsistent policy rows. -func ValidTool(tool string) bool { - switch tool { - case ToolBuildOrUpdateGraph, ToolRunPostprocess: - return true - default: - return false - } -} - -// @intent count trailing failures for one namespace and tool without crossing reset or success boundaries. -func (s *Store) consecutiveFailuresScoped(ctx context.Context, namespace, tool string, limit int) (int, error) { - var logs []model.PostprocessRunLog - if err := s.db.WithContext(ctx). - Where("namespace = ? AND tool = ?", namespace, tool). - Order("created_at desc"). - Order("id desc"). - Limit(limit). - Find(&logs).Error; err != nil { - return 0, trace.Wrap(err, "list postprocess run logs") - } - count := 0 - for _, log := range logs { - if log.Status == StatusOK { - break - } - count++ - } - return count, nil -} - -// @intent list stored postprocess policy states for the requested namespace and tool scope. -func (s *Store) listStates(ctx context.Context, namespace, tool string) ([]model.PostprocessPolicyState, error) { - query := s.db.WithContext(ctx).Model(&model.PostprocessPolicyState{}) - if namespace != "" { - query = query.Where("namespace = ?", namespace) - } - if tool != "" { - query = query.Where("tool = ?", tool) - } - var states []model.PostprocessPolicyState - if err := query.Order("updated_at desc").Find(&states).Error; err != nil { - return nil, trace.Wrap(err, "list postprocess policy states") - } - return states, nil -} - -// @intent retrieve the most recent failed runs used to explain degraded or fail-closed policy status. -func (s *Store) listLatestFailedRuns(ctx context.Context, namespace, tool string, limit int) ([]RunSnapshot, error) { - if limit <= 0 { - return nil, nil - } - var logs []model.PostprocessRunLog - if err := s.db.WithContext(ctx). - Where("namespace = ? AND tool = ? AND status <> ?", namespace, tool, StatusOK). - Order("created_at desc"). - Order("id desc"). - Limit(limit). - Find(&logs).Error; err != nil { - return nil, trace.Wrap(err, "list latest failed postprocess runs") - } - runs := make([]RunSnapshot, 0, len(logs)) - for _, log := range logs { - failed, err := unmarshalStringSlice(log.FailedSteps) - if err != nil { - return nil, err - } - skipped, err := unmarshalStringSlice(log.SkippedSteps) - if err != nil { - return nil, err - } - runs = append(runs, RunSnapshot{ - Namespace: log.Namespace, - Tool: log.Tool, - Policy: log.Policy, - Source: log.Source, - Status: log.Status, - FailedSteps: failed, - SkippedSteps: skipped, - CreatedAt: log.CreatedAt, - }) - } - return runs, nil -} - -// @intent cap run log growth per namespace and tool while preserving the newest history. -func (s *Store) pruneRunLogs(tx *gorm.DB, namespace, tool string) error { - if s.runLogRetention <= 0 { - return nil - } - for { - var staleIDs []uint - if err := tx.WithContext(context.Background()). - Model(&model.PostprocessRunLog{}). - Where("namespace = ? AND tool = ?", namespace, tool). - Order("created_at desc"). - Order("id desc"). - Offset(s.runLogRetention). - Limit(100). - Pluck("id", &staleIDs).Error; err != nil { - return trace.Wrap(err, "list stale postprocess run logs") - } - if len(staleIDs) == 0 { - return nil - } - if err := tx.Where("id IN ?", staleIDs).Delete(&model.PostprocessRunLog{}).Error; err != nil { - return trace.Wrap(err, "delete stale postprocess run logs") - } - } -} - -// @intent serialize failed and skipped step lists into the JSON strings stored in policy tables. -func marshalStringSlice(values []string) (string, error) { - if len(values) == 0 { - return "[]", nil - } - raw, err := json.Marshal(values) - if err != nil { - return "", trace.Wrap(err, "marshal string slice") - } - return string(raw), nil -} - -// @intent decode stored JSON step lists back into slices for status reporting. -func unmarshalStringSlice(raw string) ([]string, error) { - if raw == "" { - return nil, nil - } - var values []string - if err := json.Unmarshal([]byte(raw), &values); err != nil { - return nil, trace.Wrap(err, "unmarshal string slice") - } - return values, nil -} diff --git a/internal/postprocess/policy/engine_test.go b/internal/postprocess/policy/engine_test.go deleted file mode 100644 index 83e57bc..0000000 --- a/internal/postprocess/policy/engine_test.go +++ /dev/null @@ -1,372 +0,0 @@ -package policy - -import ( - "context" - "fmt" - "testing" - "time" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - - "github.com/tae2089/code-context-graph/internal/ctxns" - "github.com/tae2089/code-context-graph/internal/model" -) - -func TestEngineResolve_DefaultsToDegradedWithoutHistory(t *testing.T) { - engine := &Engine{} - policy, source, err := engine.Resolve(context.Background(), nil, DecisionInput{Tool: ToolBuildOrUpdateGraph}) - if err != nil { - t.Fatalf("resolve: %v", err) - } - if policy != PolicyDegraded { - t.Fatalf("policy = %q, want %q", policy, PolicyDegraded) - } - if source != SourceAuto { - t.Fatalf("source = %q, want %q", source, SourceAuto) - } -} - -func TestEngineResolve_ExplicitOverrideWins(t *testing.T) { - engine := &Engine{} - policy, source, err := engine.Resolve(context.Background(), nil, DecisionInput{ - Tool: ToolBuildOrUpdateGraph, - ExplicitPolicy: PolicyFailClosed, - }) - if err != nil { - t.Fatalf("resolve: %v", err) - } - if policy != PolicyFailClosed { - t.Fatalf("policy = %q, want %q", policy, PolicyFailClosed) - } - if source != SourceExplicit { - t.Fatalf("source = %q, want %q", source, SourceExplicit) - } -} - -func TestEngineResolve_EscalatesAfterThreeConsecutiveFailures(t *testing.T) { - store := setupPolicyStore(t) - engine := &Engine{} - ctx := ctxns.WithNamespace(context.Background(), "svc") - - for i := 0; i < 3; i++ { - if err := store.RecordRun(ctx, RunRecord{ - Tool: ToolBuildOrUpdateGraph, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusDegraded, - FailedSteps: []string{"communities"}, - CreatedAt: time.Unix(int64(i+1), 0), - }); err != nil { - t.Fatalf("record run %d: %v", i, err) - } - } - - policy, source, err := engine.Resolve(ctx, store, DecisionInput{Tool: ToolBuildOrUpdateGraph}) - if err != nil { - t.Fatalf("resolve: %v", err) - } - if policy != PolicyFailClosed { - t.Fatalf("policy = %q, want %q", policy, PolicyFailClosed) - } - if source != SourceAuto { - t.Fatalf("source = %q, want %q", source, SourceAuto) - } -} - -func TestStoreRecordRun_UpdatesStateAndResetsAfterSuccess(t *testing.T) { - store := setupPolicyStore(t) - ctx := ctxns.WithNamespace(context.Background(), "svc") - - for i := 0; i < 2; i++ { - if err := store.RecordRun(ctx, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusDegraded, - FailedSteps: []string{"fts"}, - CreatedAt: time.Unix(int64(i+1), 0), - }); err != nil { - t.Fatalf("record failure %d: %v", i, err) - } - } - - count, err := store.ConsecutiveFailures(ctx, ToolRunPostprocess, 3) - if err != nil { - t.Fatalf("consecutive failures after failures: %v", err) - } - if count != 2 { - t.Fatalf("count = %d, want 2", count) - } - - if err := store.RecordRun(ctx, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusOK, - CreatedAt: time.Unix(3, 0), - }); err != nil { - t.Fatalf("record success: %v", err) - } - - count, err = store.ConsecutiveFailures(ctx, ToolRunPostprocess, 3) - if err != nil { - t.Fatalf("consecutive failures after success: %v", err) - } - if count != 0 { - t.Fatalf("count = %d, want 0", count) - } - - state, err := store.GetState(ctx, ToolRunPostprocess) - if err != nil { - t.Fatalf("get state: %v", err) - } - if state == nil { - t.Fatal("expected state to exist") - } - if state.Policy != PolicyDegraded { - t.Fatalf("state policy = %q, want %q", state.Policy, PolicyDegraded) - } - if state.Namespace != "svc" { - t.Fatalf("state namespace = %q, want svc", state.Namespace) - } - if state.Tool != ToolRunPostprocess { - t.Fatalf("state tool = %q, want %q", state.Tool, ToolRunPostprocess) - } - - var logs []model.PostprocessRunLog - if err := store.db.Order("created_at asc").Find(&logs).Error; err != nil { - t.Fatalf("list logs: %v", err) - } - if len(logs) != 3 { - t.Fatalf("logs = %d, want 3", len(logs)) - } - if logs[0].FailedSteps != `["fts"]` { - t.Fatalf("first failed_steps = %s, want %s", logs[0].FailedSteps, `["fts"]`) - } - if logs[2].FailedSteps != "[]" { - t.Fatalf("success failed_steps = %s, want []", logs[2].FailedSteps) - } -} - -func TestStoreReset_InsertsResetMarkerAndClearsFailureStreak(t *testing.T) { - store := setupPolicyStore(t) - ctx := ctxns.WithNamespace(context.Background(), "svc") - - for i := 0; i < 3; i++ { - if err := store.RecordRun(ctx, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyFailClosed, - Source: SourceAuto, - Status: StatusDegraded, - FailedSteps: []string{"fts"}, - CreatedAt: time.Unix(int64(i+1), 0), - }); err != nil { - t.Fatalf("record failure %d: %v", i, err) - } - } - - if err := store.Reset(ctx, ToolRunPostprocess); err != nil { - t.Fatalf("reset: %v", err) - } - - count, err := store.ConsecutiveFailures(ctx, ToolRunPostprocess, 10) - if err != nil { - t.Fatalf("consecutive failures after reset: %v", err) - } - if count != 0 { - t.Fatalf("count = %d, want 0", count) - } - - state, err := store.GetState(ctx, ToolRunPostprocess) - if err != nil { - t.Fatalf("get state: %v", err) - } - if state == nil || state.Policy != PolicyDegraded { - t.Fatalf("state = %+v, want degraded", state) - } - - var latest model.PostprocessRunLog - if err := store.db.Order("created_at desc").Order("id desc").First(&latest).Error; err != nil { - t.Fatalf("latest run log: %v", err) - } - if latest.Source != SourceReset { - t.Fatalf("latest source = %q, want %q", latest.Source, SourceReset) - } - if latest.Status != StatusOK { - t.Fatalf("latest status = %q, want %q", latest.Status, StatusOK) - } - if latest.Policy != PolicyDegraded { - t.Fatalf("latest policy = %q, want %q", latest.Policy, PolicyDegraded) - } -} - -func TestStoreRecordRun_PrunesOldLogsPerNamespaceAndTool(t *testing.T) { - store := setupPolicyStore(t) - store.runLogRetention = 2 - - ctxA := ctxns.WithNamespace(context.Background(), "ns-a") - ctxB := ctxns.WithNamespace(context.Background(), "ns-b") - for i := 0; i < 4; i++ { - if err := store.RecordRun(ctxA, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusDegraded, - CreatedAt: time.Unix(int64(i+1), 0), - }); err != nil { - t.Fatalf("record ns-a run %d: %v", i, err) - } - } - if err := store.RecordRun(ctxA, RunRecord{ - Tool: ToolBuildOrUpdateGraph, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusOK, - CreatedAt: time.Unix(10, 0), - }); err != nil { - t.Fatalf("record ns-a other tool: %v", err) - } - if err := store.RecordRun(ctxB, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusOK, - CreatedAt: time.Unix(11, 0), - }); err != nil { - t.Fatalf("record ns-b run: %v", err) - } - - var kept []model.PostprocessRunLog - if err := store.db.Where("namespace = ? AND tool = ?", "ns-a", ToolRunPostprocess).Order("created_at asc").Find(&kept).Error; err != nil { - t.Fatalf("list retained logs: %v", err) - } - if len(kept) != 2 { - t.Fatalf("retained logs = %d, want 2", len(kept)) - } - if !kept[0].CreatedAt.Equal(time.Unix(3, 0)) || !kept[1].CreatedAt.Equal(time.Unix(4, 0)) { - t.Fatalf("unexpected retained timestamps: %+v", kept) - } - - var otherToolCount int64 - if err := store.db.Model(&model.PostprocessRunLog{}).Where("namespace = ? AND tool = ?", "ns-a", ToolBuildOrUpdateGraph).Count(&otherToolCount).Error; err != nil { - t.Fatalf("count other tool logs: %v", err) - } - if otherToolCount != 1 { - t.Fatalf("other tool logs = %d, want 1", otherToolCount) - } - - var otherNamespaceCount int64 - if err := store.db.Model(&model.PostprocessRunLog{}).Where("namespace = ? AND tool = ?", "ns-b", ToolRunPostprocess).Count(&otherNamespaceCount).Error; err != nil { - t.Fatalf("count other namespace logs: %v", err) - } - if otherNamespaceCount != 1 { - t.Fatalf("other namespace logs = %d, want 1", otherNamespaceCount) - } -} - -func TestStoreStatus_SummarizesFailClosedAndRecentFailures(t *testing.T) { - store := setupPolicyStore(t) - ctxA := ctxns.WithNamespace(context.Background(), "ns-a") - ctxB := ctxns.WithNamespace(context.Background(), "ns-b") - - for i := 0; i < 4; i++ { - if err := store.RecordRun(ctxA, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyFailClosed, - Source: SourceAuto, - Status: StatusDegraded, - FailedSteps: []string{"communities"}, - CreatedAt: time.Unix(int64(i+1), 0), - }); err != nil { - t.Fatalf("record ns-a run %d: %v", i, err) - } - } - if err := store.RecordRun(ctxB, RunRecord{ - Tool: ToolBuildOrUpdateGraph, - Policy: PolicyDegraded, - Source: SourceAuto, - Status: StatusDegraded, - FailedSteps: []string{"fts"}, - CreatedAt: time.Unix(10, 0), - }); err != nil { - t.Fatalf("record ns-b run: %v", err) - } - - summary, err := store.Status(context.Background(), StatusOptions{RecentLimit: 3}) - if err != nil { - t.Fatalf("status: %v", err) - } - if summary.Status != StatusDegraded { - t.Fatalf("summary status = %q, want %q", summary.Status, StatusDegraded) - } - if len(summary.FailClosed) != 1 { - t.Fatalf("fail_closed entries = %d, want 1", len(summary.FailClosed)) - } - if summary.FailClosed[0].Namespace != "ns-a" || summary.FailClosed[0].Tool != ToolRunPostprocess { - t.Fatalf("unexpected fail_closed entry: %+v", summary.FailClosed[0]) - } - if summary.FailClosed[0].ConsecutiveFailures != 3 { - t.Fatalf("consecutive failures = %d, want 3", summary.FailClosed[0].ConsecutiveFailures) - } - if len(summary.RecentFailures) != 3 { - t.Fatalf("recent failures = %d, want 3", len(summary.RecentFailures)) - } - if summary.RecentFailures[0].Namespace != "ns-b" { - t.Fatalf("latest recent failure namespace = %q, want ns-b", summary.RecentFailures[0].Namespace) - } -} - -func TestStoreStatus_DoesNotReportSuccessfulExplicitFailClosedRun(t *testing.T) { - store := setupPolicyStore(t) - ctx := ctxns.WithNamespace(context.Background(), "svc") - - if err := store.RecordRun(ctx, RunRecord{ - Tool: ToolRunPostprocess, - Policy: PolicyFailClosed, - Source: SourceExplicit, - Status: StatusOK, - CreatedAt: time.Unix(1, 0), - }); err != nil { - t.Fatalf("record explicit success: %v", err) - } - - state, err := store.GetState(ctx, ToolRunPostprocess) - if err != nil { - t.Fatalf("get state: %v", err) - } - if state == nil { - t.Fatal("expected state to exist") - } - if state.Policy != PolicyDegraded { - t.Fatalf("state policy = %q, want %q", state.Policy, PolicyDegraded) - } - - summary, err := store.Status(ctx, StatusOptions{Tool: ToolRunPostprocess, RecentLimit: 3}) - if err != nil { - t.Fatalf("status: %v", err) - } - if summary.Status != StatusOK { - t.Fatalf("summary status = %q, want %q", summary.Status, StatusOK) - } - if len(summary.FailClosed) != 0 { - t.Fatalf("fail_closed entries = %d, want 0", len(summary.FailClosed)) - } - if len(summary.RecentFailures) != 0 { - t.Fatalf("recent failures = %d, want 0", len(summary.RecentFailures)) - } -} - -func setupPolicyStore(t *testing.T) *Store { - t.Helper() - dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Discard}) - if err != nil { - t.Fatalf("open db: %v", err) - } - if err := db.AutoMigrate(&model.PostprocessPolicyState{}, &model.PostprocessRunLog{}); err != nil { - t.Fatalf("migrate policy tables: %v", err) - } - return NewStore(db) -} diff --git a/internal/server/serve.go b/internal/server/serve.go index d870731..a0de083 100644 --- a/internal/server/serve.go +++ b/internal/server/serve.go @@ -22,7 +22,6 @@ import ( "github.com/tae2089/code-context-graph/internal/mcpruntime" ccgobs "github.com/tae2089/code-context-graph/internal/obs" "github.com/tae2089/code-context-graph/internal/pathutil" - postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy" "github.com/tae2089/code-context-graph/internal/service" "github.com/tae2089/code-context-graph/internal/webhook" "github.com/tae2089/code-context-graph/internal/wikiserver" @@ -53,13 +52,13 @@ func Run(rt *core.Runtime, cfg Config, serviceVersion, ragIndexDir, ragProjectDe } defer inst.Close() cfg.RagIndexDir = ragIndexDir - return RunStreamableHTTP(rt, inst.Server, cfg, inst.Cache, inst.PostprocessSummary) + return RunStreamableHTTP(rt, inst.Server, cfg, inst.Cache) } // RunStreamableHTTP serves the MCP server over streamable HTTP. // @intent MCP, health, readiness, status, webhook 엔드포인트를 하나의 HTTP 런타임으로 노출한다. // @sideEffect HTTP 서버, 시그널 핸들러, 웹훅 동기화 큐를 생성하고 종료 시 drain한다. -func RunStreamableHTTP(rt *core.Runtime, srv *mcpgo.MCPServer, cfg Config, cache *mcp.Cache, postprocessSummary func(context.Context) (*postprocesspolicy.StatusSummary, error)) error { +func RunStreamableHTTP(rt *core.Runtime, srv *mcpgo.MCPServer, cfg Config, cache *mcp.Cache) error { rt.Logger.Info("serving MCP over streamable-http", "addr", cfg.HTTPAddr, "stateless", cfg.Stateless) if err := ValidateHTTPExposure(cfg); err != nil { @@ -116,7 +115,7 @@ func RunStreamableHTTP(rt *core.Runtime, srv *mcpgo.MCPServer, cfg Config, cache // so it requires the same bearer auth as /mcp; /health and /ready stay open for probes. mux.Handle("/status", MCPAuthMiddleware(cfg.HTTPBearerToken, StatusHandler(dbReadyCheck, cfg.WebhookAttemptTimeout, func() *webhook.SyncQueue { return syncQueue - }, postprocessSummary))) + }))) if cfg.WikiDir != "" { wiki, err := wikiserver.New(wikiserver.Config{ @@ -380,18 +379,17 @@ func ReadyHandler(check func(*http.Request) error) http.Handler { } // statusResponse defines the response structure for the status endpoint. -// @intent /status가 DB, webhook, postprocess 상태를 한 payload로 반환하게 한다. +// @intent /status가 DB와 webhook 상태를 한 payload로 반환하게 한다. type statusResponse struct { - Status string `json:"status"` - DB string `json:"db"` - Webhook *webhook.SyncQueueStats `json:"webhook,omitempty"` - Postprocess *postprocesspolicy.StatusSummary `json:"postprocess,omitempty"` + Status string `json:"status"` + DB string `json:"db"` + Webhook *webhook.SyncQueueStats `json:"webhook,omitempty"` } -// StatusHandler provides detailed system status including DB, webhooks, and postprocess state. +// StatusHandler provides detailed system status including DB and webhook state. // @intent 운영 진단용 상태를 종합해 HTTP 상태 코드와 JSON payload로 노출한다. -// @sideEffect DB 상태, webhook 큐 상태, 후처리 상태를 읽고 JSON 응답을 기록한다. -func StatusHandler(dbCheck func(*http.Request) error, webhookTimeout time.Duration, queue func() *webhook.SyncQueue, postprocessSummary func(context.Context) (*postprocesspolicy.StatusSummary, error)) http.Handler { +// @sideEffect DB 상태와 webhook 큐 상태를 읽고 JSON 응답을 기록한다. +func StatusHandler(dbCheck func(*http.Request) error, webhookTimeout time.Duration, queue func() *webhook.SyncQueue) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -417,18 +415,6 @@ func StatusHandler(dbCheck func(*http.Request) error, webhookTimeout time.Durati } } } - if postprocessSummary != nil { - summary, err := postprocessSummary(r.Context()) - if err == nil { - resp.Postprocess = summary - if code == http.StatusOK && summary != nil && summary.Status == postprocesspolicy.StatusDegraded { - resp.Status = "degraded" - } - } else { - slog.Error("postprocess status summary failed", "error", err) - } - } - w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) if err := json.NewEncoder(w).Encode(resp); err != nil { diff --git a/internal/store/gormstore/gormstore.go b/internal/store/gormstore/gormstore.go index a46d23f..1b60d84 100644 --- a/internal/store/gormstore/gormstore.go +++ b/internal/store/gormstore/gormstore.go @@ -43,8 +43,6 @@ func (s *Store) AutoMigrate() error { &model.CommunityMembership{}, &model.Flow{}, &model.FlowMembership{}, - &model.PostprocessPolicyState{}, - &model.PostprocessRunLog{}, ); err != nil { return err } diff --git a/internal/store/gormstore/gormstore_test.go b/internal/store/gormstore/gormstore_test.go index 30c151c..9494fcd 100644 --- a/internal/store/gormstore/gormstore_test.go +++ b/internal/store/gormstore/gormstore_test.go @@ -68,35 +68,6 @@ func TestAutoMigrate_SQLite(t *testing.T) { } } -func TestAutoMigrate_CreatesPostprocessPolicyTables(t *testing.T) { - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Discard}) - if err != nil { - t.Fatalf("failed to open db: %v", err) - } - s := New(db) - if err := s.AutoMigrate(); err != nil { - t.Fatalf("AutoMigrate failed: %v", err) - } - - if !db.Migrator().HasTable("ccg_postprocess_policy_state") { - t.Fatal("expected ccg_postprocess_policy_state table to exist") - } - if !db.Migrator().HasTable("ccg_postprocess_run_logs") { - t.Fatal("expected ccg_postprocess_run_logs table to exist") - } - - for _, column := range []string{"namespace", "tool", "policy", "updated_at"} { - if !db.Migrator().HasColumn("ccg_postprocess_policy_state", column) { - t.Fatalf("expected policy state column %q", column) - } - } - for _, column := range []string{"namespace", "tool", "policy", "source", "status", "failed_steps", "skipped_steps", "created_at"} { - if !db.Migrator().HasColumn("ccg_postprocess_run_logs", column) { - t.Fatalf("expected policy log column %q", column) - } - } -} - func TestUpsertNodes_Insert(t *testing.T) { s := setupTestDB(t) ctx := context.Background()