-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add anomaly detection pipeline #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dfaf6dd
66f6ff5
a32ca20
f06e834
37253b2
8b292f5
2fd942a
660433e
57ec36b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ClickHouse connection details | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLICKHOUSE_HOST=localhost | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLICKHOUSE_PORT=8123 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLICKHOUSE_USER=default | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLICKHOUSE_PASSWORD=secret | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLICKHOUSE_DB=cwv_monitor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AI_ANALYST_CLICKHOUSE_PASSWORD= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BETTER_AUTH_SECRET= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INITIAL_USER_EMAIL= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INITIAL_USER_PASSWORD= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INITIAL_USER_NAME= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Application | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOG_LEVEL=info | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NODE_ENV=development | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AUTH_BASE_URL=http://localhost:3000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Anomaly Detection Webhooks (Optional) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SLACK_WEBHOOK_URL= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TEAMS_WEBHOOK_URL= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: git ls-files | grep -E "apps/anomaly-worker/\.env"Repository: Blazity/next-cwv-monitor Length of output: 125 🏁 Script executed: if [ -f "apps/anomaly-worker/.env" ]; then echo "=== .env exists ==="; wc -l apps/anomaly-worker/.env; else echo ".env not found"; fi
if [ -f "apps/anomaly-worker/.env.example" ]; then echo "=== .env.example exists ==="; wc -l apps/anomaly-worker/.env.example; else echo ".env.example not found"; fiRepository: Blazity/next-cwv-monitor Length of output: 179 🏁 Script executed: if [ -f "apps/anomaly-worker/.env" ] && [ -f "apps/anomaly-worker/.env.example" ]; then
echo "=== Content of .env ==="; cat -n apps/anomaly-worker/.env
echo
echo "=== Content of .env.example ==="; cat -n apps/anomaly-worker/.env.example
fiRepository: Blazity/next-cwv-monitor Length of output: 1156 🏁 Script executed: #!/bin/bash
set -euo pipefail
if [ ! -f "apps/anomaly-worker/.env" ] || [ ! -f "apps/anomaly-worker/.env.example" ]; then
echo "One or both files not found"
exit 1
fi
echo "Keys in apps/anomaly-worker/.env but missing in apps/anomaly-worker/.env.example:"
comm -23 \
<(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort) \
<(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort)
echo
echo "Keys in .env.example but missing in .env:"
comm -13 \
<(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort) \
<(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort)Repository: Blazity/next-cwv-monitor Length of output: 210 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Keys in .env ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort
echo
echo "=== Keys in .env.example ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort
echo
echo "=== Missing from .env.example (in .env but not in example) ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort > /tmp/env_keys.txt
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort > /tmp/env_example_keys.txt
comm -23 /tmp/env_keys.txt /tmp/env_example_keys.txtRepository: Blazity/next-cwv-monitor Length of output: 666 🏁 Script executed: # Check if missing variables are required at runtime or have defaults in the code
cd apps/anomaly-worker && (
echo "=== Checking references to missing variables ==="
for var in AI_ANALYST_CLICKHOUSE_PASSWORD BETTER_AUTH_SECRET INITIAL_USER_EMAIL INITIAL_USER_NAME INITIAL_USER_PASSWORD; do
echo "Searching for $var..."
rg "$var" --type ts --type js --type json 2>/dev/null | head -5 || echo " Not found in code"
done
)Repository: Blazity/next-cwv-monitor Length of output: 942
The template omits 5 variables present in Required additions to .env.example # ClickHouse connection details
CLICKHOUSE_HOST=localhost
CLICKHOUSE_PORT=8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=cwv_monitor
+AI_ANALYST_CLICKHOUSE_PASSWORD=
+BETTER_AUTH_SECRET=
+INITIAL_USER_EMAIL=
+INITIAL_USER_PASSWORD=
+INITIAL_USER_NAME=
# Application
LOG_LEVEL=info
NODE_ENV=development
AUTH_BASE_URL=http://localhost:3000📝 Committable suggestion
Suggested change
🧰 Tools🪛 dotenv-linter (4.0.0)[warning] 5-5: [UnorderedKey] The CLICKHOUSE_PASSWORD key should go before the CLICKHOUSE_PORT key (UnorderedKey) [warning] 6-6: [UnorderedKey] The CLICKHOUSE_DB key should go before the CLICKHOUSE_HOST key (UnorderedKey) [warning] 11-11: [UnorderedKey] The AUTH_BASE_URL key should go before the LOG_LEVEL key (UnorderedKey) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .env |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| { | ||
| "name": "anomaly-worker", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "description": "A lightweight worker for detecting anomalies.", | ||
| "main": "dist/anomaly-worker/src/index.js", | ||
| "type": "module", | ||
| "scripts": { | ||
| "prebuild": "rm -rf dist", | ||
| "build": "tsc", | ||
| "start": "cross-env NODE_OPTIONS=\"-r dotenv/config\" tsx src/index.ts", | ||
| "dev": "tsx src/index.ts | pino-pretty", | ||
| "test": "vitest", | ||
| "test:watch": "vitest --watch" | ||
| }, | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "ISC", | ||
| "dependencies": { | ||
| "@clickhouse/client": "^1.14.0", | ||
| "@t3-oss/env-nextjs": "^0.13.8", | ||
| "arktype": "^2.1.28", | ||
| "date-fns": "^4.1.0", | ||
| "node-cron": "^3.0.3", | ||
| "pino": "^10.1.0", | ||
| "remeda": "^2.32.0", | ||
| "waddler": "^0.1.1", | ||
| "zod": "^4.1.13", | ||
| "dotenv": "^16.4.5" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24", | ||
| "@types/node-cron": "^3.0.11", | ||
| "pino-pretty": "^13.0.0", | ||
| "tsx": "^4.19.2", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.15", | ||
| "tsconfig-paths": "^4.2.0", | ||
| "cross-env": "^7.0.3" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { createEnv } from "@t3-oss/env-nextjs"; | ||
| import { z } from "zod"; | ||
|
|
||
| const LOG_LEVELS = ["fatal", "error", "warn", "info", "debug", "trace", "silent"] as const; | ||
|
|
||
| export const env = createEnv({ | ||
| server: { | ||
| AUTH_BASE_URL: z.url(), | ||
| TRUST_PROXY: z.enum(["true", "false"]).default("false"), | ||
| CLICKHOUSE_HOST: z.string().min(1, "CLICKHOUSE_HOST is required"), | ||
| CLICKHOUSE_PORT: z.string().min(1, "CLICKHOUSE_PORT is required"), | ||
| CLICKHOUSE_USER: z.string().min(1, "CLICKHOUSE_USER is required"), | ||
| CLICKHOUSE_PASSWORD: z.string(), | ||
| CLICKHOUSE_DB: z.string().min(1, "CLICKHOUSE_DB is required"), | ||
| AI_ANALYST_CLICKHOUSE_USER: z.string().min(1).default("ai_analyst_user"), | ||
| AI_ANALYST_CLICKHOUSE_PASSWORD: z.string().min(1), | ||
| BETTER_AUTH_SECRET: z.string(), | ||
| CLICKHOUSE_ADAPTER_DEBUG_LOGS: z.coerce.boolean().default(false), | ||
| MIN_PASSWORD_SCORE: z.coerce.number().min(0).max(4).default(2), | ||
| RATE_LIMIT_WINDOW_MS: z.coerce.number().positive().default(60_000), | ||
| MAX_LOGIN_ATTEMPTS: z.coerce.number().positive().default(5), | ||
| INITIAL_USER_EMAIL: z.email(), | ||
| INITIAL_USER_PASSWORD: z.string().min(8), | ||
| INITIAL_USER_NAME: z.string().min(3), | ||
| NODE_ENV: z.enum(["development", "test", "production"]).default("development"), | ||
| LOG_LEVEL: z.enum(LOG_LEVELS).default("info"), | ||
| SLACK_WEBHOOK_URL: z.url().optional(), | ||
| TEAMS_WEBHOOK_URL: z.url().optional(), | ||
| }, | ||
| client: {}, | ||
| runtimeEnv: { | ||
| AUTH_BASE_URL: process.env.AUTH_BASE_URL, | ||
| TRUST_PROXY: process.env.TRUST_PROXY, | ||
| CLICKHOUSE_HOST: process.env.CLICKHOUSE_HOST, | ||
| CLICKHOUSE_PORT: process.env.CLICKHOUSE_PORT, | ||
| CLICKHOUSE_USER: process.env.CLICKHOUSE_USER, | ||
| CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD, | ||
| CLICKHOUSE_DB: process.env.CLICKHOUSE_DB, | ||
| AI_ANALYST_CLICKHOUSE_USER: process.env.AI_ANALYST_CLICKHOUSE_USER, | ||
| AI_ANALYST_CLICKHOUSE_PASSWORD: process.env.AI_ANALYST_CLICKHOUSE_PASSWORD, | ||
| BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, | ||
| CLICKHOUSE_ADAPTER_DEBUG_LOGS: process.env.CLICKHOUSE_ADAPTER_DEBUG_LOGS, | ||
| MIN_PASSWORD_SCORE: process.env.MIN_PASSWORD_SCORE, | ||
| RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS, | ||
| MAX_LOGIN_ATTEMPTS: process.env.MAX_LOGIN_ATTEMPTS, | ||
| INITIAL_USER_EMAIL: process.env.INITIAL_USER_EMAIL, | ||
| INITIAL_USER_PASSWORD: process.env.INITIAL_USER_PASSWORD, | ||
| INITIAL_USER_NAME: process.env.INITIAL_USER_NAME, | ||
| NODE_ENV: process.env.NODE_ENV, | ||
| LOG_LEVEL: process.env.LOG_LEVEL, | ||
| SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL, | ||
| TEAMS_WEBHOOK_URL: process.env.TEAMS_WEBHOOK_URL, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import pino from 'pino'; | ||
| import cron from 'node-cron'; | ||
| import { env } from "./env"; | ||
|
|
||
| const logger = pino({ name: 'anomaly-worker', level: env.LOG_LEVEL }); | ||
|
|
||
| let isRunning = false; | ||
| let isShuttingDown = false; | ||
|
|
||
| async function runDetectionCycle() { | ||
| if (isRunning || isShuttingDown) { | ||
| return; | ||
| } | ||
|
|
||
| isRunning = true; | ||
| logger.info("Starting anomaly detection cycle..."); | ||
|
|
||
| try { | ||
| const { NotificationsService } = await import('@monitor-app/app/server/domain/notifications/service'); | ||
| const service = new NotificationsService(); | ||
|
|
||
| await service.notifyNewAnomalies(); | ||
| logger.info("Anomaly detection cycle finished."); | ||
| } catch (error) { | ||
| logger.error({ err: error }, "Error during detection cycle."); | ||
| } finally { | ||
| isRunning = false; | ||
| } | ||
| } | ||
|
|
||
| const task = cron.schedule('30 * * * *', runDetectionCycle); | ||
| logger.info("Anomaly worker scheduled (30 * * * *)."); | ||
|
|
||
| runDetectionCycle(); | ||
|
|
||
| process.on('SIGTERM', async () => { | ||
| isShuttingDown = true; | ||
| task.stop(); | ||
|
|
||
| if (isRunning) { | ||
| logger.info("Waiting for the current detection cycle to complete..."); | ||
|
|
||
| const shutdownTimeout = setTimeout(() => { | ||
| logger.error("Shutdown timed out; forcing exit."); | ||
| process.exit(1); | ||
| }, 20000); | ||
|
|
||
| while (isRunning) { | ||
| await new Promise(resolve => setTimeout(resolve, 500)); | ||
| } | ||
|
|
||
| clearTimeout(shutdownTimeout); | ||
| } | ||
|
|
||
| logger.info("Shutdown complete."); | ||
| process.exit(0); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "outDir": "./dist", | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "paths": { | ||
| "@/*": ["./src/*", "../../apps/monitor-app/src/*"], | ||
| "@monitor-app/*": ["../monitor-app/src/*"] | ||
| } | ||
| }, | ||
| "include": ["src/**/*.ts"], | ||
| "exclude": ["node_modules"] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||
| # AI Agent Architecture & Integration Guide | ||||||||||||||||||
|
|
||||||||||||||||||
| This document outlines the bidirectional AI monitoring system for the CWV Monitor. It explains how the **Monitor App** acts as the analytical engine for **AI Agent (Slack/Teams Bot)**. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## 1. System Components | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Monitor App (`apps/monitor-app`) | ||||||||||||||||||
| The "Source of Truth" and Data Engine. | ||||||||||||||||||
| - **ClickHouse**: Stores all raw performance events and pre-aggregated metrics. | ||||||||||||||||||
| - **AiBridge**: A domain service that provides safe, high-level methods for the AI Agent to query data. | ||||||||||||||||||
| - **Schema Catalog**: A YAML definition (`clickhouse/catalog.yml`) that describes the database to the LLM. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### AI Agent | ||||||||||||||||||
| The interaction layer (Slack/Teams). | ||||||||||||||||||
| - **Inbound Handling**: Receives webhooks and message events from Slack/Teams. | ||||||||||||||||||
| - **LLM Brain**: Uses the `AiBridge` from the Monitor App to fetch context and execute diagnostic SQL. | ||||||||||||||||||
| - **Conversation State**: Tracks thread IDs and maps them to specific `anomaly_id`s. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Anomaly Worker (`apps/anomaly-worker`) | ||||||||||||||||||
| The proactive trigger. | ||||||||||||||||||
| - Runs on a cron schedule. | ||||||||||||||||||
| - Scans ClickHouse for statistical anomalies (Z-Score > 3). | ||||||||||||||||||
| - Triggers the initial outbound notification via the Monitor App's `NotificationsService`. | ||||||||||||||||||
|
|
||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| ## 2. Security Model | ||||||||||||||||||
|
|
||||||||||||||||||
| We follow the principle of **Least Privilege** for AI interactions: | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Restricted User**: The Agent uses `ai_analyst_user` in ClickHouse. | ||||||||||||||||||
| - **Role-Based Access**: The `r_ai_analyst` role is limited to `SELECT` on core tables and `INSERT/UPDATE` only on the `processed_anomalies` audit table. | ||||||||||||||||||
| - **Resource Guardrails**: The `ai_analyst_profile` sets hard limits on memory (2GB), execution time (15s), and rows read (100M) to prevent the LLM from generating "expensive" or runaway queries. | ||||||||||||||||||
|
|
||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| ## 3. Capabilities ("What can be done") | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Outbound Alerts | ||||||||||||||||||
| When an anomaly is detected, the `NotificationsService` sends a rich payload to Slack/Teams: | ||||||||||||||||||
| - **Investigate Button**: Links to the Monitor App Web UI. | ||||||||||||||||||
| - **Chat with AI Button**: Deep-links to the AI Agent to start a diagnostic conversation. | ||||||||||||||||||
| - **Metadata**: Includes `anomalyId`, `projectId`, and `metricName` so the Agent knows exactly what to talk about. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Deep-Dive Context | ||||||||||||||||||
| The AI Agent can call `aiBridge.getAnomalyContext(anomalyId)` to receive a "Context Bundle": | ||||||||||||||||||
| 1. **Anomaly Record**: Specifics about the z-score and metric. | ||||||||||||||||||
| 2. **Project Info**: Domain and configuration. | ||||||||||||||||||
| 3. **24h Trend**: Pre-fetched hourly averages for the last 24 hours. | ||||||||||||||||||
| 4. **Schema Reference**: The `catalog.yml` content to help the LLM generate its own SQL. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Dynamic SQL Execution | ||||||||||||||||||
| The Agent can ask the LLM to "Verify if this happens on other routes." The LLM generates a SQL query, and the Agent executes it via `aiBridge.executeSql(query, params)`. | ||||||||||||||||||
|
|
||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| ## 4. Integration Pattern | ||||||||||||||||||
|
|
||||||||||||||||||
| To use the Monitor App domain in your AI application: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```typescript | ||||||||||||||||||
| import { aiBridge } from "@monitor-app/app/server/domain/ai/service"; | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect import path in documentation. The import path references 📝 Proposed fix-import { aiBridge } from "@monitor-app/app/server/domain/ai/service";
+import { aiBridge } from "@monitor-app/app/server/domain/ai-bridge/service";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // 1. When a user clicks "Chat with AI" in Slack | ||||||||||||||||||
| async function handleChatStart(anomalyId: string) { | ||||||||||||||||||
| // Get everything the LLM needs to know in one call | ||||||||||||||||||
| const context = await aiBridge.getAnomalyContext(anomalyId); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Provide this context to your LLM system prompt | ||||||||||||||||||
| const systemPrompt = ` | ||||||||||||||||||
| You are a Performance Expert. | ||||||||||||||||||
| Analyze this anomaly: ${JSON.stringify(context.anomaly)} | ||||||||||||||||||
| Database Schema: ${context.schemaReference} | ||||||||||||||||||
| ... | ||||||||||||||||||
| `; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // 2. When the LLM wants to fetch more data | ||||||||||||||||||
| async function onLlmToolUse(generatedSql: string) { | ||||||||||||||||||
| const results = await aiBridge.executeSql(generatedSql); | ||||||||||||||||||
| return results; | ||||||||||||||||||
| } | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ## 5. Files Reference | ||||||||||||||||||
| - **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml` | ||||||||||||||||||
| - **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai/service.ts` | ||||||||||||||||||
| - **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/` | ||||||||||||||||||
|
Comment on lines
+85
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect file path in reference. The file reference should point to 📝 Proposed fix ## 5. Files Reference
- **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml`
-- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai/service.ts`
+- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai-bridge/service.ts`
- **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/`📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diagram target for notification state is inconsistent with the text.
Line 42 shows “marks as notified” going to
Raw, but Line 93 documentsprocessed_anomaliesas the state store. Please align the diagram with the documented table.Also applies to: 93-94
🤖 Prompt for AI Agents