# AGENTS.md - regional-insight-server

## Project Overview

regional-insight-server is a weekly fleet inspection service for observability regions: K8s right-sizing, node cost analysis, and capacity risk detection, shipped as a Next.js app with a web UI (fleet dashboard, region detail, observe, run history, run-pair compare) and Prometheus metrics. Alert rules are deferred. `src/engine/` is the sole, idiomatic implementation of the analysis pipeline; correctness is enforced by the compiler (strict types, no `any`), hand-verified assertions over a synthetic cluster corpus, and the golden suite that freezes the engine's outputs at a relative tolerance of 1e-9.

**Stack:** Next.js 16 (App Router, Turbopack) · React 19 · TypeScript strict · Tailwind 4 · recharts · prom-client · p-limit
**Testing:** `npm test` (vitest, jsdom) — no external services required; tests run on a synthetic cluster corpus (`tests/engine/synthetic/`, no recorded production data), server tests use temp dirs, component tests use fabricated fixtures
**Location:** subdirectory of the `tidbcloud/observability` monorepo — run `git` from the repo root (two levels up) or prefix paths with `regional-insight-server/`

## Build & Run

```bash
npm install
npm run dev            # next dev on :3000 — scheduler does NOT run in dev
npm run build && npm run start   # production — instrumentation starts the scheduler
npm test               # vitest run (goldens + semantics + server + component tests)
npm run typecheck      # tsc --noEmit, strict
npm run lint           # eslint
npm run region -- <region-key>   # single-region engine smoke → data/manual/<region>/
npm run sync-config    # regenerate config/fleet.json from vmcluster env dirs
npm run goldens        # check engine outputs vs golden fixtures (-- --force re-records)
```

The scheduler only starts when `NODE_ENV=production` (`src/instrumentation.ts`) — `npm run dev` never schedules runs; use `npm run build && npm run start` to exercise scheduling. Pre-push gate: `npm run typecheck && npm run lint && npm test`, all green.

## Architecture Invariants

- Layering runs one direction only: `src/engine/` (pure analysis core) ← `src/server/` (orchestration) ← `src/app/` (pages and API routes); `src/lib/` and `src/components/` support the app layer. The engine has **zero next/react imports and zero I/O** (no fs/network/env) — keep it portable. Pricing's loaders/fetchers/URL caches live in `src/server/pricing-io.ts`.
- Engine data contracts are fully declared in `src/engine/types.ts` (no index signatures, no `any`); field names are camelCase end to end (API wire, on-disk artifacts, UI). The golden fixture suite pins the serialized shape — exact key-set equality — and `tests/conventions/naming.spec.ts` guards against snake_case regressions.
- External vocabularies stay snake_case by contract: Prometheus **metric names** (`regional_insight_*`), PromQL strings and KSM label keys (`label_eks_amazonaws_com_nodegroup`, `owner_kind`, ...), vendor storage values (`cloud_essd`), and the API error-code enums (`not_found`, `bad_request`, `conflict`, `upstream_error`). They are allowlisted in the naming guard.
- Rounding goes through `src/engine/round.ts` (`roundTo`, ties half-up). Display formatting (`src/lib/fmt.ts`) uses Intl/toFixed directly — display rounding may differ from engine rounding on exact ties, which is fine at rendering precision.
- Placement behavior in `src/engine/shrink.ts` (best-fit-decreasing placement, safe-demand capacity, critical N+1) is golden-pinned; changing the strategy is a deliberate, reviewed change. `src/engine/classify.ts` stays I/O-free.
- The test corpus is **synthetic and code-owned** (`tests/engine/synthetic/`): specs are hand-authored, `build-payloads.ts` derives query strings and the vmcluster workload set from the engine's own exported constants/classifiers, and a two-way assertion pins built keys === consumed keys. No recorded production data in git; evolving the PromQL surface never needs dataplane access (add a builder case — the pinned-literal and two-way tests fail until you do).
- All run persistence goes through `src/server/store.ts` — atomic `.tmp`+rename writes, boot recovery rewrites stale "running" headers to "aborted". Never write run artifacts elsewhere.
- `data/pricing.json` has a **single writer**: the fleet pricing preflight in `src/server/fleet-runner.ts`. The region runner treats it read-only.
- Process singletons live on `globalThis` (`src/server/runtime.ts`, `src/server/metrics.ts`) because Next dev instantiates modules twice — do not remove those guards.
- Prometheus metric names are contractual (`regional_insight_*` prefix, `src/server/metrics.ts`) — renaming them requires monitoring-side changes.
- API routes use `export const dynamic = "force-dynamic"` and the `{error:{code,message}}` envelope (`src/lib/api-error.ts`); pages are `"use client"` SPAs fetching same-origin via `src/lib/api-client.ts` (ingress strips the `/regional-insight` prefix).
- Generated or vendored, never hand-edited: `config/fleet.json` (regenerated by `npm run sync-config`), `tests/fixtures/` goldens (regenerated by `npm run goldens -- --force`; the corpus inputs are the code in `tests/engine/synthetic/`), `config/datasource.json` (vendored, keeps its upstream shape — mapped at the resolve boundary; never used for fleet enumeration). Hand-authored and boot-validated instead: `config/shrink-policy.json` (mirrors the engine's `DEFAULT_SHRINK_POLICY`, pinned by tests) and `config/machine-catalog.json` (hand-reviewed SKU table: adjacency edges, provenance URLs, and review dates are a human workflow — the two-way test pins the file to the engine defaults). The `data/pricing.json` schema is owned by this repo (`config/pricing-seed.json` seeds it). `data/` is runtime state — never committed.
- Fleet enumeration comes from `config/fleet.json` only, never `datasource.json` (`src/server/fleet.ts`).
- All wall-clock times are UTC+8 (Asia/Shanghai, fixed offset). Run IDs use `run-YYYYMMDDTHHMMSS` (UTC+8 wall clock, no zone suffix) so lexicographic order equals chronological order — do not change the format. Stored timestamps carry an explicit `+08:00` offset; ordering/retention code must compare via `Date.parse`, never string comparison.

## Project Constraints

- **English is the project's sole language.** This is a global-team project: every artifact authored for it — code, comments, commit messages, docs, plans (including local-only `.claude/plans/`), test names and descriptions, UI copy, PR/issue text — is written in English, without exception. Monorepo CI rejects Chinese characters outside `skills/` and `.md` files; hold plan files and other local artifacts to the same standard even though CI cannot see them.
- `@typescript-eslint/no-explicit-any` is **enforced**; `src/engine/values.ts` (getOr/toFloat/toInt) is the one sanctioned coercion boundary for duck-typed JSON.
- Minimize external dependencies; only add one when the benefit clearly outweighs the maintenance cost.
- **Never trigger fleet runs or `run-region` against the real dataplane** unless the user explicitly asks for a live run in this session. These hit real clusters (read-only, but heavy). Read-only API usage is safe.
- Runtime env vars: `SVC_ROOT`, `DATA_DIR` (default `<root>/data`), `SCHEDULE_CRON` (default `0 2 * * 1`, Mon 02:00 UTC+8 — cron fields are Asia/Shanghai wall clock), `LOOKBACK_DAYS` (7), `REGION_CONCURRENCY` (8). All are validated at boot — bad values fail fast.
- **Single-process service by design**: the run store's write serialization, the pricing single-writer, and run-id generation assume one process owns `DATA_DIR`. Do not run two replicas (or a `run-region` smoke) against the same production `DATA_DIR`.

## Commit Conventions

All commits use `Conventional Commits` prefixes with scope `regional-insight-server`. Select the type based on the actual change scope:

| Type       | When to use                                      |
| ---------- | ------------------------------------------------ |
| `feat`     | New user-facing feature or capability            |
| `fix`      | Bug fix or correctness correction                |
| `docs`     | Documentation-only changes                       |
| `style`    | Formatting, whitespace (no logic)                |
| `refactor` | Code restructure with no feature/behavior change |
| `perf`     | Performance improvement                          |
| `test`     | Add or update tests                              |
| `build`    | Build system or dependency changes               |
| `ci`       | CI/CD pipeline or config changes                 |
| `chore`    | Maintenance, tooling, or miscellaneous           |
| `revert`   | Revert a previous commit                         |

Format:

```
<type>(regional-insight-server): <description>

[optional body]
```

Body guidance:
- **Optional.** If the subject line tells the whole story, omit the body.
- The body is a supplement — high-signal context a reviewer needs *before* reading the diff (motivation, surprising constraint), not a walkthrough of the diff.
- Every commit includes a `Co-Authored-By:` trailer.

## Plan Execution Rules

### 1. Planning

Write plans to `.claude/plans/` as markdown files **before writing any code**. File name format: `$date-$name.md` (e.g. `2026-09-04-manual-trigger-route.md`). Plans are local-only and never committed.

Rules:
- At least 3 phases; each phase is a self-contained, testable increment with concrete file paths and a validation method.
- The last phase is always **Review**: two passes over the full diff against `main` — `/code-review` first (bugs and correctness), then `/simplify` (conventions and simplicity). Apply each pass's worthwhile fixes and commit them separately.
- The first `/code-review` pass must not spawn git-history agents (subagents mining `git log` / `git blame` / past commits) — they are too slow. That pass reviews the current diff and code only; consult git history in a later pass only if a finding genuinely requires it.
- Scope phases to minimize rollback cost.

### 2. Execution

Before making changes, create a new branch off `main` (`feat/<short-name>` / `fix/<short-name>`). Once execution starts, do not stop until all phases are complete; resolve blockers without handing the work back unfinished.

For each phase, complete all four steps **before moving to the next phase**:

1. **Implement** the phase changes.
2. **Verify**: `npm run typecheck && npm run lint && npm test` — all must pass. For API or UI changes, also run `npm run dev` and curl the touched route (or load the page) to confirm behavior.
3. **Commit** per the conventions above — one commit per phase, no batching.
4. **Mark the phase done** in the plan file before starting the next phase.

### 3. Post-Plan

1. **Verify** with a final `npm run typecheck && npm run lint && npm test`.
2. **Summarize** end-to-end (`git log` + `git diff --stat` against `main`): what was built, key decisions, files modified, suggested next steps.
3. **Merge**: ask the user whether to merge back to `main`. PRs always target `main`, never another branch.
