orgchat 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
orgchat-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: orgchat
3
+ Version: 0.1.0
4
+ Summary: Production-readiness checks for organization chatbot projects
5
+ Author: Amin
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # OrgChat Check
11
+
12
+ یک CLI سبک برای اینکه قبل از ساخت یا تحویل چت‌بات سازمانی بفهمیم پروژه در چه وضعیتی است.
13
+
14
+ دستور اصلی:
15
+
16
+ ```bash
17
+ orgchat check
18
+ ```
19
+
20
+ این دستور هفت سؤال مهم را بررسی می‌کند:
21
+
22
+ 1. داده‌ها کجا هستند و چطور به‌روز می‌شوند؟
23
+ 2. هر کاربر به چه اطلاعاتی دسترسی دارد؟
24
+ 3. پاسخ درست را چطور اندازه می‌گیریم؟
25
+ 4. اگر مدل جواب را نداند چه می‌کند؟
26
+ 5. هزینه و latency را چطور می‌بینیم؟
27
+ 6. وقتی سیستم خراب شد چه کسی آن را برمی‌گرداند؟
28
+ 7. بعد از تحویل چه کسی سیستم را نگهداری می‌کند؟
29
+
30
+ ## نصب محلی
31
+
32
+ Python 3.11 یا بالاتر لازم است.
33
+
34
+ ```bash
35
+ python -m venv .venv
36
+ source .venv/bin/activate # Linux/macOS
37
+ # .venv\Scripts\activate # Windows PowerShell
38
+ pip install -e .
39
+ ```
40
+
41
+ ## شروع سریع
42
+
43
+ در ریشهٔ پروژه‌ای که می‌خواهی بررسی کنی:
44
+
45
+ ```bash
46
+ orgchat init
47
+ ```
48
+
49
+ مقادیر `orgchat.toml` را پر کن و بعد:
50
+
51
+ ```bash
52
+ orgchat check
53
+ ```
54
+
55
+ برای خروجی فایل:
56
+
57
+ ```bash
58
+ orgchat check --format markdown --output orgchat-report.md
59
+ orgchat check --format json --output orgchat-report.json
60
+ ```
61
+
62
+ برای بررسی یک مسیر دیگر:
63
+
64
+ ```bash
65
+ orgchat check --path ./my-chatbot
66
+ ```
67
+
68
+ برای CI:
69
+
70
+ ```bash
71
+ orgchat check --strict
72
+ ```
73
+
74
+ کد خروجی:
75
+
76
+ - `0`: همهٔ کنترل‌ها عبور کرده‌اند یا فقط هشدار غیرstrict وجود دارد.
77
+ - `1`: حداقل یک کنترل fail شده، یا در حالت `--strict` هشدار وجود دارد.
78
+ - `2`: خطای ورودی، مسیر یا تنظیمات.
79
+
80
+ ## منطق گزارش
81
+
82
+ `orgchat.toml` منبع اصلی ارزیابی است. مقدارهای صریح `false` یا مسیرهای اشتباه باعث `FAIL` می‌شوند. مقدارهای ثبت‌نشده `WARN` می‌گیرند. CLI در کنار تنظیمات، نام فایل‌ها و پوشه‌های رایج مثل `evals`، `retrieval`، `monitoring` و `runbook` را هم به‌عنوان شواهد کمکی پیدا می‌کند.
83
+
84
+ شواهد کشف‌شده به‌تنهایی جایگزین تنظیم صریح کنترل‌ها نیستند؛ هدفشان این است که گزارش اولیه از پروژهٔ موجود مفید باشد و مسیر تکمیل را نشان بدهد.
85
+
86
+ ## نمونه خروجی
87
+
88
+ ```text
89
+ ORGCHAT CHECK
90
+ Project: Enterprise Support Chatbot
91
+ Overall: WARN | Score: 64.5/100
92
+
93
+ Checks:
94
+ [PASS] 100.0/100 داده و به‌روزبودن
95
+ [WARN] 55.0/100 ارزیابی کیفیت
96
+ [FAIL] 40.0/100 دسترسی و امنیت
97
+
98
+ Next actions:
99
+ 1. دسترسی و امنیت: بازیابی را با مجوزهای همان کاربر فیلتر کن.
100
+ ```
101
+
102
+ ## ساختار پروژه
103
+
104
+ ```text
105
+ src/orgchat/
106
+ checker.py # منطق هفت کنترل
107
+ config.py # خواندن TOML و مسیرها
108
+ report.py # خروجی terminal/json/markdown
109
+ cli.py # دستورات orgchat check و orgchat init
110
+ tests/
111
+ examples/
112
+ ```
113
+
114
+ این نسخه وضعیت آمادگی عملیاتی را گزارش می‌کند و جایگزین تست امنیتی، ارزیابی مدل یا ممیزی سازمانی کامل نیست.
@@ -0,0 +1,105 @@
1
+ # OrgChat Check
2
+
3
+ یک CLI سبک برای اینکه قبل از ساخت یا تحویل چت‌بات سازمانی بفهمیم پروژه در چه وضعیتی است.
4
+
5
+ دستور اصلی:
6
+
7
+ ```bash
8
+ orgchat check
9
+ ```
10
+
11
+ این دستور هفت سؤال مهم را بررسی می‌کند:
12
+
13
+ 1. داده‌ها کجا هستند و چطور به‌روز می‌شوند؟
14
+ 2. هر کاربر به چه اطلاعاتی دسترسی دارد؟
15
+ 3. پاسخ درست را چطور اندازه می‌گیریم؟
16
+ 4. اگر مدل جواب را نداند چه می‌کند؟
17
+ 5. هزینه و latency را چطور می‌بینیم؟
18
+ 6. وقتی سیستم خراب شد چه کسی آن را برمی‌گرداند؟
19
+ 7. بعد از تحویل چه کسی سیستم را نگهداری می‌کند؟
20
+
21
+ ## نصب محلی
22
+
23
+ Python 3.11 یا بالاتر لازم است.
24
+
25
+ ```bash
26
+ python -m venv .venv
27
+ source .venv/bin/activate # Linux/macOS
28
+ # .venv\Scripts\activate # Windows PowerShell
29
+ pip install -e .
30
+ ```
31
+
32
+ ## شروع سریع
33
+
34
+ در ریشهٔ پروژه‌ای که می‌خواهی بررسی کنی:
35
+
36
+ ```bash
37
+ orgchat init
38
+ ```
39
+
40
+ مقادیر `orgchat.toml` را پر کن و بعد:
41
+
42
+ ```bash
43
+ orgchat check
44
+ ```
45
+
46
+ برای خروجی فایل:
47
+
48
+ ```bash
49
+ orgchat check --format markdown --output orgchat-report.md
50
+ orgchat check --format json --output orgchat-report.json
51
+ ```
52
+
53
+ برای بررسی یک مسیر دیگر:
54
+
55
+ ```bash
56
+ orgchat check --path ./my-chatbot
57
+ ```
58
+
59
+ برای CI:
60
+
61
+ ```bash
62
+ orgchat check --strict
63
+ ```
64
+
65
+ کد خروجی:
66
+
67
+ - `0`: همهٔ کنترل‌ها عبور کرده‌اند یا فقط هشدار غیرstrict وجود دارد.
68
+ - `1`: حداقل یک کنترل fail شده، یا در حالت `--strict` هشدار وجود دارد.
69
+ - `2`: خطای ورودی، مسیر یا تنظیمات.
70
+
71
+ ## منطق گزارش
72
+
73
+ `orgchat.toml` منبع اصلی ارزیابی است. مقدارهای صریح `false` یا مسیرهای اشتباه باعث `FAIL` می‌شوند. مقدارهای ثبت‌نشده `WARN` می‌گیرند. CLI در کنار تنظیمات، نام فایل‌ها و پوشه‌های رایج مثل `evals`، `retrieval`، `monitoring` و `runbook` را هم به‌عنوان شواهد کمکی پیدا می‌کند.
74
+
75
+ شواهد کشف‌شده به‌تنهایی جایگزین تنظیم صریح کنترل‌ها نیستند؛ هدفشان این است که گزارش اولیه از پروژهٔ موجود مفید باشد و مسیر تکمیل را نشان بدهد.
76
+
77
+ ## نمونه خروجی
78
+
79
+ ```text
80
+ ORGCHAT CHECK
81
+ Project: Enterprise Support Chatbot
82
+ Overall: WARN | Score: 64.5/100
83
+
84
+ Checks:
85
+ [PASS] 100.0/100 داده و به‌روزبودن
86
+ [WARN] 55.0/100 ارزیابی کیفیت
87
+ [FAIL] 40.0/100 دسترسی و امنیت
88
+
89
+ Next actions:
90
+ 1. دسترسی و امنیت: بازیابی را با مجوزهای همان کاربر فیلتر کن.
91
+ ```
92
+
93
+ ## ساختار پروژه
94
+
95
+ ```text
96
+ src/orgchat/
97
+ checker.py # منطق هفت کنترل
98
+ config.py # خواندن TOML و مسیرها
99
+ report.py # خروجی terminal/json/markdown
100
+ cli.py # دستورات orgchat check و orgchat init
101
+ tests/
102
+ examples/
103
+ ```
104
+
105
+ این نسخه وضعیت آمادگی عملیاتی را گزارش می‌کند و جایگزین تست امنیتی، ارزیابی مدل یا ممیزی سازمانی کامل نیست.
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "orgchat"
7
+ version = "0.1.0"
8
+ description = "Production-readiness checks for organization chatbot projects"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Amin" }]
13
+ dependencies = []
14
+
15
+ [project.scripts]
16
+ orgchat = "orgchat.cli:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """OrgChat project readiness checker."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date, datetime, timedelta, timezone
5
+ from pathlib import Path
6
+ from typing import Any, Callable
7
+
8
+ from .config import load_config, resolve_project_path, section
9
+ from .models import CheckResult, Finding, Report
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Signal:
14
+ key: str
15
+ label: str
16
+ kind: str
17
+ points: float
18
+ recommendation: str
19
+ field: str
20
+
21
+
22
+ CHECK_DEFINITIONS: list[dict[str, Any]] = [
23
+ {
24
+ "key": "data_sources",
25
+ "title": "داده و به‌روزبودن",
26
+ "question": "داده‌ها کجا هستند و چطور به‌روز می‌شوند؟",
27
+ "patterns": ["minio", "s3", "storage", "ingest", "sync", "etl", "pipeline", "freshness"],
28
+ "signals": [
29
+ Signal("sources", "منابع داده مشخص شده‌اند", "list", 25, "منبع‌های واقعی مثل MinIO، PostgreSQL یا S3 را در orgchat.toml ثبت کن.", "sources"),
30
+ Signal("freshness_slo_minutes", "حد به‌روزبودن داده مشخص است", "number", 20, "برای داده‌ها یک بازهٔ مجاز تأخیر تعیین کن.", "freshness_slo_minutes"),
31
+ Signal("sync_monitoring", "همگام‌سازی پایش می‌شود", "bool", 20, "برای شکست یا عقب‌افتادن ingestion هشدار تعریف کن.", "sync_monitoring"),
32
+ Signal("owner", "مالک داده مشخص است", "text", 20, "یک مالک مشخص برای صحت و به‌روزبودن داده تعیین کن.", "owner"),
33
+ Signal("refresh_test", "تست تازه‌سازی وجود دارد", "bool", 15, "یک تست قابل‌اجرا برای refresh یا ingestion اضافه کن.", "refresh_test"),
34
+ ],
35
+ },
36
+ {
37
+ "key": "access_control",
38
+ "title": "دسترسی و امنیت",
39
+ "question": "هر کاربر به چه اطلاعاتی دسترسی دارد؟",
40
+ "patterns": ["auth", "rbac", "acl", "permission", "tenant", "jwt", "oidc", "oauth", "audit"],
41
+ "signals": [
42
+ Signal("auth", "احراز هویت فعال است", "text", 20, "یک روش احراز هویت سازمانی مثل OIDC، OAuth یا JWT ثبت کن.", "auth"),
43
+ Signal("rbac", "نقش‌ها و مجوزها تعریف شده‌اند", "bool", 25, "RBAC را برای نقش‌های کاربر، مدیر و اپراتور تعریف کن.", "rbac"),
44
+ Signal("document_level_filters", "فیلتر دسترسی در سطح سند وجود دارد", "bool", 25, "بازیابی را با مجوزهای همان کاربر فیلتر کن.", "document_level_filters"),
45
+ Signal("audit_log", "گزارش دسترسی ثبت می‌شود", "bool", 15, "رویدادهای دسترسی و پاسخ‌های حساس را audit کن.", "audit_log"),
46
+ Signal("secret_scan", "کنترل نشت secrets انجام می‌شود", "bool", 15, "Secret scanning را در مخزن و CI فعال کن.", "secret_scan"),
47
+ ],
48
+ },
49
+ {
50
+ "key": "quality",
51
+ "title": "ارزیابی کیفیت",
52
+ "question": "پاسخ درست را چطور اندازه می‌گیریم؟",
53
+ "patterns": ["eval", "golden", "benchmark", "quality", "regression"],
54
+ "signals": [
55
+ Signal("eval_dataset", "مجموعهٔ ارزیابی وجود دارد", "path", 25, "یک golden dataset واقعی از سؤال‌ها و پاسخ‌های مورد انتظار بساز.", "eval_dataset"),
56
+ Signal("automated_eval", "ارزیابی خودکار اجرا می‌شود", "bool", 25, "ارزیابی را در CI یا یک job زمان‌بندی‌شده اجرا کن.", "automated_eval"),
57
+ Signal("regression_gate", "گیت جلوگیری از regression وجود دارد", "bool", 20, "اجازهٔ deploy را به عبور از حداقل امتیاز مشروط کن.", "regression_gate"),
58
+ Signal("min_score", "حداقل امتیاز کیفیت تعیین شده", "number", 15, "حداقل امتیاز قابل‌قبول را مشخص کن.", "min_score"),
59
+ Signal("last_evaluation", "ارزیابی اخیر است", "recent", 15, "نتیجهٔ ارزیابی را مرتباً اجرا و تاریخ آخرین اجرا را ثبت کن.", "last_evaluation"),
60
+ ],
61
+ },
62
+ {
63
+ "key": "grounding",
64
+ "title": "پاسخ مستند و fallback",
65
+ "question": "اگر مدل جواب را نداند یا داده کافی نباشد چه می‌کند؟",
66
+ "patterns": ["retrieval", "citation", "source", "fallback", "abstain", "rerank"],
67
+ "signals": [
68
+ Signal("citations", "پاسخ منبع و citation دارد", "bool", 25, "منبع سند و بخش مرتبط را همراه پاسخ نمایش بده.", "citations"),
69
+ Signal("abstention", "مدل می‌تواند از پاسخ‌سازی خودداری کند", "bool", 25, "برای نبود شواهد، پاسخ صادقانهٔ «اطلاعات کافی نیست» تعریف کن.", "abstention"),
70
+ Signal("fallback", "مسیر fallback انسانی یا عملیاتی وجود دارد", "bool", 20, "پاسخ‌های کم‌اطمینان را به اپراتور یا مسیر جایگزین بفرست.", "fallback"),
71
+ Signal("prompt_injection_defense", "دفاع در برابر prompt injection وجود دارد", "bool", 15, "ورودی سند و کاربر را از دستورهای سیستمی جدا و تست کن.", "prompt_injection_defense"),
72
+ Signal("retrieval_logging", "فرایند بازیابی قابل‌ردگیری است", "bool", 15, "سندها، chunkها و score بازیابی‌شده را برای عیب‌یابی ثبت کن.", "retrieval_logging"),
73
+ ],
74
+ },
75
+ {
76
+ "key": "observability",
77
+ "title": "هزینه و مشاهده‌پذیری",
78
+ "question": "هزینه، latency و کیفیت سرویس را چطور می‌بینیم؟",
79
+ "patterns": ["telemetry", "tracing", "metrics", "prometheus", "cost", "token", "latency"],
80
+ "signals": [
81
+ Signal("tracing", "trace هر درخواست وجود دارد", "bool", 20, "برای هر درخواست trace از ورودی تا retrieval و generation بساز.", "tracing"),
82
+ Signal("token_cost_tracking", "هزینهٔ token یا inference ثبت می‌شود", "bool", 25, "هزینه را به پروژه، کاربر یا تیم نسبت بده.", "token_cost_tracking"),
83
+ Signal("request_metrics", "متریک درخواست وجود دارد", "bool", 20, "تعداد درخواست، خطا، latency و نرخ fallback را اندازه بگیر.", "request_metrics"),
84
+ Signal("latency_slo_ms", "SLO پاسخ مشخص است", "number", 15, "یک latency هدف برای پاسخ تعیین کن.", "latency_slo_ms"),
85
+ Signal("dashboard", "داشبورد قابل‌استفاده وجود دارد", "path", 20, "یک داشبورد برای وضعیت و روندهای سرویس نگه‌دار.", "dashboard"),
86
+ ],
87
+ },
88
+ {
89
+ "key": "monitoring",
90
+ "title": "مانیتورینگ و بازیابی",
91
+ "question": "وقتی سیستم خراب شد چه کسی و با چه روشی آن را برمی‌گرداند؟",
92
+ "patterns": ["monitor", "health", "alert", "sentry", "incident", "watchdog", "backup"],
93
+ "signals": [
94
+ Signal("healthcheck", "health check تعریف شده", "text", 20, "یک endpoint یا دستور health check قابل‌اجرا ثبت کن.", "healthcheck"),
95
+ Signal("error_logging", "خطاها ثبت می‌شوند", "bool", 25, "خطاهای backend، retrieval و مدل را با context کافی ذخیره کن.", "error_logging"),
96
+ Signal("alerts", "هشدار عملیاتی وجود دارد", "bool", 20, "برای خطا، latency، هزینه و افت کیفیت alert بساز.", "alerts"),
97
+ Signal("backup_restore_test", "بازیابی تست شده است", "bool", 15, "backup را فقط نگیر؛ restore آن را دوره‌ای تست کن.", "backup_restore_test"),
98
+ Signal("incident_runbook", "راهنمای incident وجود دارد", "path", 20, "برای خرابی مدل، منبع داده و سرویس یک runbook بنویس.", "incident_runbook"),
99
+ ],
100
+ },
101
+ {
102
+ "key": "ownership",
103
+ "title": "مالکیت و نگهداری",
104
+ "question": "بعد از تحویل، چه کسی سیستم را نگهداری می‌کند؟",
105
+ "patterns": ["owner", "maintainer", "runbook", "oncall", "operations", "docs"],
106
+ "signals": [
107
+ Signal("service_owner", "مالک سرویس مشخص است", "text", 25, "یک تیم یا فرد پاسخ‌گو برای سرویس تعیین کن.", "service_owner"),
108
+ Signal("technical_owner", "مالک فنی مشخص است", "text", 20, "مسئول تصمیم‌های فنی و تغییرات معماری را مشخص کن.", "technical_owner"),
109
+ Signal("oncall", "مسیر on-call مشخص است", "text", 20, "برای incident یک مسیر پاسخ‌گویی واقعی تعیین کن.", "oncall"),
110
+ Signal("runbook", "runbook اصلی وجود دارد", "path", 20, "نحوهٔ deploy، rollback، عیب‌یابی و refresh را مستند کن.", "runbook"),
111
+ Signal("review_cadence", "دورهٔ بازبینی مشخص است", "text", 15, "بازبینی ماهانهٔ کیفیت، امنیت و هزینه را در تقویم بگذار.", "review_cadence"),
112
+ ],
113
+ },
114
+ ]
115
+
116
+
117
+ IGNORED_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build"}
118
+
119
+
120
+ def discover_paths(root: Path, limit: int = 6000) -> list[str]:
121
+ paths: list[str] = []
122
+ try:
123
+ iterator = root.rglob("*")
124
+ for path in iterator:
125
+ if len(paths) >= limit:
126
+ break
127
+ if any(part in IGNORED_DIRS for part in path.parts):
128
+ continue
129
+ if path.is_file():
130
+ paths.append(path.relative_to(root).as_posix().lower())
131
+ except OSError:
132
+ return paths
133
+ return paths
134
+
135
+
136
+ def _evidence(paths: list[str], patterns: list[str]) -> list[str]:
137
+ matches = []
138
+ for path in paths:
139
+ if path.endswith((".md", ".txt")) and path not in {"readme.md", "readme.txt"}:
140
+ pass
141
+ if any(pattern in path for pattern in patterns):
142
+ matches.append(path)
143
+ return matches[:5]
144
+
145
+
146
+ def _as_bool(value: Any) -> bool | None:
147
+ if isinstance(value, bool):
148
+ return value
149
+ if isinstance(value, str):
150
+ normalized = value.strip().lower()
151
+ if normalized in {"true", "yes", "1", "on"}:
152
+ return True
153
+ if normalized in {"false", "no", "0", "off"}:
154
+ return False
155
+ return None
156
+
157
+
158
+ def _parse_datetime(value: Any) -> datetime | None:
159
+ if not isinstance(value, str) or not value.strip():
160
+ return None
161
+ text = value.strip().replace("Z", "+00:00")
162
+ try:
163
+ parsed = datetime.fromisoformat(text)
164
+ except ValueError:
165
+ try:
166
+ parsed = datetime.combine(date.fromisoformat(text), datetime.min.time())
167
+ except ValueError:
168
+ return None
169
+ if parsed.tzinfo is None:
170
+ parsed = parsed.replace(tzinfo=timezone.utc)
171
+ return parsed.astimezone(timezone.utc)
172
+
173
+
174
+ def _assess(
175
+ root: Path,
176
+ values: dict[str, Any],
177
+ signal: Signal,
178
+ evidence: list[str],
179
+ ) -> tuple[str, float, str]:
180
+ present = signal.field in values
181
+ value = values.get(signal.field)
182
+
183
+ if signal.kind == "bool":
184
+ parsed = _as_bool(value)
185
+ if parsed is True:
186
+ return "PASS", signal.points, "فعال است."
187
+ if parsed is False:
188
+ return "FAIL", 0.0, "صراحتاً غیرفعال تنظیم شده است."
189
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "در تنظیمات مشخص نشده است." if not present else "مقدار بولی قابل‌تشخیص نیست."
190
+
191
+ if signal.kind == "list":
192
+ if isinstance(value, list) and value:
193
+ return "PASS", signal.points, f"{len(value)} مورد ثبت شده است."
194
+ if present and isinstance(value, list):
195
+ return "FAIL", 0.0, "فهرست خالی است."
196
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "در تنظیمات مشخص نشده است."
197
+
198
+ if signal.kind == "text":
199
+ if isinstance(value, str) and value.strip():
200
+ return "PASS", signal.points, "ثبت شده است."
201
+ if present:
202
+ return "FAIL", 0.0, "خالی است."
203
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "در تنظیمات مشخص نشده است."
204
+
205
+ if signal.kind == "path":
206
+ path = resolve_project_path(root, value)
207
+ if path and path.exists():
208
+ try:
209
+ display_path = path.relative_to(root).as_posix()
210
+ except ValueError:
211
+ display_path = str(path)
212
+ return "PASS", signal.points, f"فایل پیدا شد: {display_path}"
213
+ if present and isinstance(value, str) and value.strip():
214
+ return "FAIL", 0.0, f"مسیر پیدا نشد: {value}"
215
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "مسیر در تنظیمات مشخص نشده است."
216
+
217
+ if signal.kind == "number":
218
+ try:
219
+ number = float(value)
220
+ except (TypeError, ValueError):
221
+ number = 0.0
222
+ if number > 0:
223
+ return "PASS", signal.points, f"مقدار {number:g} ثبت شده است."
224
+ if present:
225
+ return "FAIL", 0.0, "مقدار باید بزرگ‌تر از صفر باشد."
226
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "در تنظیمات مشخص نشده است."
227
+
228
+ if signal.kind == "recent":
229
+ parsed = _parse_datetime(value)
230
+ if parsed is None:
231
+ if present and value:
232
+ return "FAIL", 0.0, "تاریخ قابل‌خواندن نیست."
233
+ return "WARN", signal.points * 0.35 if evidence else 0.0, "تاریخ آخرین اجرا ثبت نشده است."
234
+ age = datetime.now(timezone.utc) - parsed
235
+ if age <= timedelta(days=30) and age >= timedelta(days=-1):
236
+ return "PASS", signal.points, f"آخرین اجرا {age.days} روز قبل بوده است."
237
+ return "FAIL", 0.0, f"آخرین اجرا {age.days} روز قبل بوده و stale است."
238
+
239
+ return "WARN", 0.0, "نوع بررسی ناشناخته است."
240
+
241
+
242
+ def _evaluate_check(root: Path, config: dict[str, Any], paths: list[str], definition: dict[str, Any], use_heuristics: bool) -> CheckResult:
243
+ values = section(config, definition["key"])
244
+ evidence = _evidence(paths, definition["patterns"]) if use_heuristics else []
245
+ findings: list[Finding] = []
246
+
247
+ for signal in definition["signals"]:
248
+ status, points, message = _assess(root, values, signal, evidence)
249
+ findings.append(
250
+ Finding(
251
+ key=signal.key,
252
+ label=signal.label,
253
+ status=status,
254
+ points=round(points, 2),
255
+ max_points=signal.points,
256
+ message=message,
257
+ recommendation=signal.recommendation,
258
+ evidence=evidence[:3] if status != "PASS" else [],
259
+ )
260
+ )
261
+
262
+ score = round(sum(finding.points for finding in findings), 2)
263
+ max_score = sum(finding.max_points for finding in findings)
264
+ if any(finding.status == "FAIL" for finding in findings):
265
+ status = "FAIL"
266
+ elif any(finding.status == "WARN" for finding in findings):
267
+ status = "WARN"
268
+ else:
269
+ status = "PASS"
270
+
271
+ return CheckResult(
272
+ key=definition["key"],
273
+ title=definition["title"],
274
+ question=definition["question"],
275
+ status=status,
276
+ score=score,
277
+ max_score=max_score,
278
+ findings=findings,
279
+ evidence=evidence,
280
+ )
281
+
282
+
283
+ def build_report(root: str | Path, config_path: str | Path | None = None, use_heuristics: bool = True) -> Report:
284
+ project_root = Path(root).expanduser().resolve()
285
+ if not project_root.exists() or not project_root.is_dir():
286
+ raise ValueError(f"Project path is not a directory: {project_root}")
287
+
288
+ requested_config = Path(config_path).expanduser() if config_path else None
289
+ config, loaded_path, loaded = load_config(project_root, requested_config)
290
+ paths = discover_paths(project_root) if use_heuristics else []
291
+ checks = [_evaluate_check(project_root, config, paths, definition, use_heuristics) for definition in CHECK_DEFINITIONS]
292
+
293
+ total = sum(check.score for check in checks)
294
+ maximum = sum(check.max_score for check in checks)
295
+ score = round((total / maximum) * 100, 1) if maximum else 0.0
296
+ if any(check.status == "FAIL" for check in checks):
297
+ overall_status = "FAIL"
298
+ elif any(check.status == "WARN" for check in checks):
299
+ overall_status = "WARN"
300
+ else:
301
+ overall_status = "PASS"
302
+
303
+ failed = sum(check.status == "FAIL" for check in checks)
304
+ warned = sum(check.status == "WARN" for check in checks)
305
+ if overall_status == "PASS":
306
+ analysis = "هر هفت محور اصلی کنترل شده‌اند و شواهد لازم در تنظیمات پروژه ثبت شده است."
307
+ elif overall_status == "FAIL":
308
+ analysis = f"پروژه هنوز آمادهٔ تحویل سازمانی نیست؛ {failed} محور دارای شکاف جدی و {warned} محور نیازمند تکمیل است."
309
+ elif not loaded:
310
+ analysis = "فایل orgchat.toml پیدا نشد؛ این گزارش فقط یک baseline اولیه است و برای ارزیابی واقعی باید تنظیمات پروژه ثبت شود."
311
+ else:
312
+ analysis = f"پروژه در وضعیت میانی است؛ {warned} محور نیازمند تکمیل است و هنوز FAIL جدی ثبت نشده."
313
+
314
+ actions: list[str] = []
315
+ for check in checks:
316
+ for finding in check.findings:
317
+ if finding.status != "PASS":
318
+ actions.append(f"{check.title}: {finding.recommendation}")
319
+ actions = actions[:8]
320
+
321
+ project_section = section(config, "project")
322
+ project_name = project_section.get("name") if isinstance(project_section.get("name"), str) else project_root.name
323
+ config_display = None
324
+ if loaded_path:
325
+ try:
326
+ config_display = loaded_path.relative_to(project_root).as_posix()
327
+ except ValueError:
328
+ config_display = str(loaded_path)
329
+
330
+ return Report(
331
+ project=project_name or project_root.name,
332
+ root=str(project_root),
333
+ config_file=config_display,
334
+ config_loaded=loaded,
335
+ generated_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
336
+ overall_status=overall_status,
337
+ score=score,
338
+ analysis=analysis,
339
+ checks=checks,
340
+ next_actions=actions,
341
+ )
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .checker import build_report
9
+ from .report import render
10
+ from .templates import CONFIG_TEMPLATE
11
+
12
+
13
+ def _parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ prog="orgchat",
16
+ description="Check production readiness of an organization chatbot project.",
17
+ )
18
+ parser.add_argument("--version", action="version", version=f"orgchat-check {__version__}")
19
+ subparsers = parser.add_subparsers(dest="command")
20
+
21
+ check = subparsers.add_parser("check", help="Run the seven organization-chatbot readiness checks.")
22
+ check.add_argument("--path", default=".", help="Project root to inspect. Default: current directory")
23
+ check.add_argument("--config", help="Path to an orgchat.toml file")
24
+ check.add_argument("--format", choices=["terminal", "json", "markdown"], default="terminal")
25
+ check.add_argument("--output", help="Write the rendered report to this file")
26
+ check.add_argument("--no-heuristics", action="store_true", help="Only use explicit orgchat.toml values")
27
+ check.add_argument("--strict", action="store_true", help="Return exit code 1 for WARN as well as FAIL")
28
+
29
+ init = subparsers.add_parser("init", help="Create a starter orgchat.toml file.")
30
+ init.add_argument("--path", default=".", help="Project root. Default: current directory")
31
+ init.add_argument("--force", action="store_true", help="Overwrite an existing orgchat.toml")
32
+ return parser
33
+
34
+
35
+ def _write_report(content: str, output: str) -> None:
36
+ destination = Path(output).expanduser().resolve()
37
+ destination.parent.mkdir(parents=True, exist_ok=True)
38
+ destination.write_text(content, encoding="utf-8")
39
+
40
+
41
+ def _run_init(path: str, force: bool) -> int:
42
+ root = Path(path).expanduser().resolve()
43
+ root.mkdir(parents=True, exist_ok=True)
44
+ destination = root / "orgchat.toml"
45
+ if destination.exists() and not force:
46
+ print(f"File already exists: {destination}. Use --force to overwrite.", file=sys.stderr)
47
+ return 1
48
+ destination.write_text(CONFIG_TEMPLATE, encoding="utf-8")
49
+ print(f"Created {destination}")
50
+ print("Run `orgchat check` after filling the values.")
51
+ return 0
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> int:
55
+ parser = _parser()
56
+ args = parser.parse_args(argv)
57
+
58
+ if args.command == "init":
59
+ return _run_init(args.path, args.force)
60
+
61
+ if args.command != "check":
62
+ parser.print_help()
63
+ return 2
64
+
65
+ try:
66
+ report = build_report(
67
+ root=args.path,
68
+ config_path=args.config,
69
+ use_heuristics=not args.no_heuristics,
70
+ )
71
+ content = render(report, args.format)
72
+ if args.output:
73
+ _write_report(content, args.output)
74
+ print(f"Report written to {Path(args.output).expanduser().resolve()}")
75
+ else:
76
+ print(content, end="")
77
+ except (OSError, ValueError) as exc:
78
+ print(f"orgchat check failed: {exc}", file=sys.stderr)
79
+ return 2
80
+
81
+ if report.overall_status == "FAIL":
82
+ return 1
83
+ if args.strict and report.overall_status == "WARN":
84
+ return 1
85
+ return 0
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ class ConfigError(ValueError):
9
+ """Raised when orgchat.toml cannot be loaded."""
10
+
11
+
12
+ def load_config(root: Path, config_path: Path | None = None) -> tuple[dict[str, Any], Path | None, bool]:
13
+ path = config_path or (root / "orgchat.toml")
14
+ if not path.is_absolute():
15
+ path = (root / path).resolve()
16
+
17
+ if not path.exists():
18
+ return {}, None, False
19
+
20
+ try:
21
+ with path.open("rb") as handle:
22
+ data = tomllib.load(handle)
23
+ except tomllib.TOMLDecodeError as exc:
24
+ raise ConfigError(f"Invalid TOML in {path}: {exc}") from exc
25
+ except OSError as exc:
26
+ raise ConfigError(f"Cannot read {path}: {exc}") from exc
27
+
28
+ if not isinstance(data, dict):
29
+ raise ConfigError(f"The configuration root must be a TOML table: {path}")
30
+ return data, path, True
31
+
32
+
33
+ def section(config: dict[str, Any], name: str) -> dict[str, Any]:
34
+ value = config.get(name, {})
35
+ return value if isinstance(value, dict) else {}
36
+
37
+
38
+ def resolve_project_path(root: Path, value: Any) -> Path | None:
39
+ if not isinstance(value, str) or not value.strip():
40
+ return None
41
+ text = value.strip()
42
+ if text.startswith(("http://", "https://", "cmd:", "/health", "health://")):
43
+ return None
44
+ candidate = Path(text)
45
+ if not candidate.is_absolute():
46
+ candidate = root / candidate
47
+ return candidate.resolve()
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Finding:
9
+ key: str
10
+ label: str
11
+ status: str
12
+ points: float
13
+ max_points: float
14
+ message: str
15
+ recommendation: str
16
+ evidence: list[str] = field(default_factory=list)
17
+
18
+ @property
19
+ def percentage(self) -> float:
20
+ if not self.max_points:
21
+ return 0.0
22
+ return round((self.points / self.max_points) * 100, 1)
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ value = asdict(self)
26
+ value["percentage"] = self.percentage
27
+ return value
28
+
29
+
30
+ @dataclass
31
+ class CheckResult:
32
+ key: str
33
+ title: str
34
+ question: str
35
+ status: str
36
+ score: float
37
+ max_score: float
38
+ findings: list[Finding] = field(default_factory=list)
39
+ evidence: list[str] = field(default_factory=list)
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ value = asdict(self)
43
+ value["findings"] = [finding.to_dict() for finding in self.findings]
44
+ value["percentage"] = round((self.score / self.max_score) * 100, 1) if self.max_score else 0.0
45
+ return value
46
+
47
+
48
+ @dataclass
49
+ class Report:
50
+ project: str
51
+ root: str
52
+ config_file: str | None
53
+ config_loaded: bool
54
+ generated_at: str
55
+ overall_status: str
56
+ score: float
57
+ analysis: str
58
+ checks: list[CheckResult]
59
+ next_actions: list[str]
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ return {
63
+ "project": self.project,
64
+ "root": self.root,
65
+ "config_file": self.config_file,
66
+ "config_loaded": self.config_loaded,
67
+ "generated_at": self.generated_at,
68
+ "overall_status": self.overall_status,
69
+ "score": self.score,
70
+ "analysis": self.analysis,
71
+ "checks": [check.to_dict() for check in self.checks],
72
+ "next_actions": self.next_actions,
73
+ }
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .models import CheckResult, Report
7
+
8
+
9
+ def _status(status: str) -> str:
10
+ return {"PASS": "PASS", "WARN": "WARN", "FAIL": "FAIL"}.get(status, status)
11
+
12
+
13
+ def render_terminal(report: Report) -> str:
14
+ lines = [
15
+ "ORGCHAT CHECK",
16
+ f"Project: {report.project}",
17
+ f"Root: {report.root}",
18
+ f"Config: {report.config_file or 'orgchat.toml پیدا نشد'}",
19
+ "",
20
+ f"Overall: {_status(report.overall_status)} | Score: {report.score}/100",
21
+ f"Analysis: {report.analysis}",
22
+ "",
23
+ "Checks:",
24
+ ]
25
+ for check in report.checks:
26
+ lines.append(f" [{_status(check.status):4}] {check.score:5.1f}/{check.max_score:g} {check.title}")
27
+ lines.append(f" {check.question}")
28
+ for finding in check.findings:
29
+ if finding.status != "PASS":
30
+ lines.append(f" - [{finding.status}] {finding.label}: {finding.message}")
31
+ if check.evidence:
32
+ lines.append(f" Evidence: {', '.join(check.evidence[:3])}")
33
+
34
+ if report.next_actions:
35
+ lines.extend(["", "Next actions:"])
36
+ lines.extend(f" {index}. {action}" for index, action in enumerate(report.next_actions, start=1))
37
+
38
+ lines.extend(["", f"Generated: {report.generated_at}"])
39
+ return "\n".join(lines)
40
+
41
+
42
+ def render_markdown(report: Report) -> str:
43
+ lines = [
44
+ f"# OrgChat Check: {report.project}",
45
+ "",
46
+ f"- وضعیت کلی: **{report.overall_status}**",
47
+ f"- امتیاز: **{report.score}/100**",
48
+ f"- تحلیل: {report.analysis}",
49
+ f"- مسیر پروژه: `{report.root}`",
50
+ f"- تنظیمات: `{report.config_file or 'orgchat.toml پیدا نشد'}`",
51
+ "",
52
+ "## خلاصهٔ بررسی‌ها",
53
+ "",
54
+ "| وضعیت | امتیاز | محور | سؤال اصلی |",
55
+ "|---|---:|---|---|",
56
+ ]
57
+ for check in report.checks:
58
+ lines.append(f"| {check.status} | {check.score:.1f}/{check.max_score:g} | {check.title} | {check.question} |")
59
+
60
+ for check in report.checks:
61
+ lines.extend(["", f"## {check.title}", "", f"سؤال: {check.question}", ""])
62
+ for finding in check.findings:
63
+ lines.append(f"- **{finding.status}** {finding.label}: {finding.message}")
64
+ if finding.status != "PASS":
65
+ lines.append(f" - اقدام: {finding.recommendation}")
66
+ if check.evidence:
67
+ lines.append(f"- شواهد پیدا‌شده: `{', '.join(check.evidence)}`")
68
+
69
+ if report.next_actions:
70
+ lines.extend(["", "## اقدام‌های بعدی", ""])
71
+ lines.extend(f"{index}. {action}" for index, action in enumerate(report.next_actions, start=1))
72
+ return "\n".join(lines) + "\n"
73
+
74
+
75
+ def render_json(report: Report) -> str:
76
+ return json.dumps(report.to_dict(), ensure_ascii=False, indent=2) + "\n"
77
+
78
+
79
+ def render(report: Report, output_format: str) -> str:
80
+ if output_format == "json":
81
+ return render_json(report)
82
+ if output_format == "markdown":
83
+ return render_markdown(report)
84
+ return render_terminal(report)
@@ -0,0 +1,56 @@
1
+ CONFIG_TEMPLATE = '''# OrgChat readiness configuration
2
+ # Run `orgchat check` from the project root after filling these values.
3
+
4
+ [project]
5
+ name = "My organization chatbot"
6
+ environment = "staging"
7
+
8
+ [data_sources]
9
+ sources = []
10
+ freshness_slo_minutes = 1440
11
+ sync_monitoring = false
12
+ owner = ""
13
+ refresh_test = false
14
+
15
+ [access_control]
16
+ auth = ""
17
+ rbac = false
18
+ document_level_filters = false
19
+ audit_log = false
20
+ secret_scan = false
21
+
22
+ [quality]
23
+ eval_dataset = "evals/golden.jsonl"
24
+ automated_eval = false
25
+ regression_gate = false
26
+ min_score = 0.8
27
+ last_evaluation = ""
28
+
29
+ [grounding]
30
+ citations = false
31
+ abstention = false
32
+ fallback = false
33
+ prompt_injection_defense = false
34
+ retrieval_logging = false
35
+
36
+ [observability]
37
+ tracing = false
38
+ token_cost_tracking = false
39
+ request_metrics = false
40
+ latency_slo_ms = 2000
41
+ dashboard = "ops/dashboard.json"
42
+
43
+ [monitoring]
44
+ healthcheck = "/health"
45
+ error_logging = false
46
+ alerts = false
47
+ backup_restore_test = false
48
+ incident_runbook = "docs/incident-runbook.md"
49
+
50
+ [ownership]
51
+ service_owner = ""
52
+ technical_owner = ""
53
+ oncall = ""
54
+ runbook = "docs/runbook.md"
55
+ review_cadence = "monthly"
56
+ '''
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: orgchat
3
+ Version: 0.1.0
4
+ Summary: Production-readiness checks for organization chatbot projects
5
+ Author: Amin
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # OrgChat Check
11
+
12
+ یک CLI سبک برای اینکه قبل از ساخت یا تحویل چت‌بات سازمانی بفهمیم پروژه در چه وضعیتی است.
13
+
14
+ دستور اصلی:
15
+
16
+ ```bash
17
+ orgchat check
18
+ ```
19
+
20
+ این دستور هفت سؤال مهم را بررسی می‌کند:
21
+
22
+ 1. داده‌ها کجا هستند و چطور به‌روز می‌شوند؟
23
+ 2. هر کاربر به چه اطلاعاتی دسترسی دارد؟
24
+ 3. پاسخ درست را چطور اندازه می‌گیریم؟
25
+ 4. اگر مدل جواب را نداند چه می‌کند؟
26
+ 5. هزینه و latency را چطور می‌بینیم؟
27
+ 6. وقتی سیستم خراب شد چه کسی آن را برمی‌گرداند؟
28
+ 7. بعد از تحویل چه کسی سیستم را نگهداری می‌کند؟
29
+
30
+ ## نصب محلی
31
+
32
+ Python 3.11 یا بالاتر لازم است.
33
+
34
+ ```bash
35
+ python -m venv .venv
36
+ source .venv/bin/activate # Linux/macOS
37
+ # .venv\Scripts\activate # Windows PowerShell
38
+ pip install -e .
39
+ ```
40
+
41
+ ## شروع سریع
42
+
43
+ در ریشهٔ پروژه‌ای که می‌خواهی بررسی کنی:
44
+
45
+ ```bash
46
+ orgchat init
47
+ ```
48
+
49
+ مقادیر `orgchat.toml` را پر کن و بعد:
50
+
51
+ ```bash
52
+ orgchat check
53
+ ```
54
+
55
+ برای خروجی فایل:
56
+
57
+ ```bash
58
+ orgchat check --format markdown --output orgchat-report.md
59
+ orgchat check --format json --output orgchat-report.json
60
+ ```
61
+
62
+ برای بررسی یک مسیر دیگر:
63
+
64
+ ```bash
65
+ orgchat check --path ./my-chatbot
66
+ ```
67
+
68
+ برای CI:
69
+
70
+ ```bash
71
+ orgchat check --strict
72
+ ```
73
+
74
+ کد خروجی:
75
+
76
+ - `0`: همهٔ کنترل‌ها عبور کرده‌اند یا فقط هشدار غیرstrict وجود دارد.
77
+ - `1`: حداقل یک کنترل fail شده، یا در حالت `--strict` هشدار وجود دارد.
78
+ - `2`: خطای ورودی، مسیر یا تنظیمات.
79
+
80
+ ## منطق گزارش
81
+
82
+ `orgchat.toml` منبع اصلی ارزیابی است. مقدارهای صریح `false` یا مسیرهای اشتباه باعث `FAIL` می‌شوند. مقدارهای ثبت‌نشده `WARN` می‌گیرند. CLI در کنار تنظیمات، نام فایل‌ها و پوشه‌های رایج مثل `evals`، `retrieval`، `monitoring` و `runbook` را هم به‌عنوان شواهد کمکی پیدا می‌کند.
83
+
84
+ شواهد کشف‌شده به‌تنهایی جایگزین تنظیم صریح کنترل‌ها نیستند؛ هدفشان این است که گزارش اولیه از پروژهٔ موجود مفید باشد و مسیر تکمیل را نشان بدهد.
85
+
86
+ ## نمونه خروجی
87
+
88
+ ```text
89
+ ORGCHAT CHECK
90
+ Project: Enterprise Support Chatbot
91
+ Overall: WARN | Score: 64.5/100
92
+
93
+ Checks:
94
+ [PASS] 100.0/100 داده و به‌روزبودن
95
+ [WARN] 55.0/100 ارزیابی کیفیت
96
+ [FAIL] 40.0/100 دسترسی و امنیت
97
+
98
+ Next actions:
99
+ 1. دسترسی و امنیت: بازیابی را با مجوزهای همان کاربر فیلتر کن.
100
+ ```
101
+
102
+ ## ساختار پروژه
103
+
104
+ ```text
105
+ src/orgchat/
106
+ checker.py # منطق هفت کنترل
107
+ config.py # خواندن TOML و مسیرها
108
+ report.py # خروجی terminal/json/markdown
109
+ cli.py # دستورات orgchat check و orgchat init
110
+ tests/
111
+ examples/
112
+ ```
113
+
114
+ این نسخه وضعیت آمادگی عملیاتی را گزارش می‌کند و جایگزین تست امنیتی، ارزیابی مدل یا ممیزی سازمانی کامل نیست.
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/orgchat/__init__.py
4
+ src/orgchat/__main__.py
5
+ src/orgchat/checker.py
6
+ src/orgchat/cli.py
7
+ src/orgchat/config.py
8
+ src/orgchat/models.py
9
+ src/orgchat/report.py
10
+ src/orgchat/templates.py
11
+ src/orgchat.egg-info/PKG-INFO
12
+ src/orgchat.egg-info/SOURCES.txt
13
+ src/orgchat.egg-info/dependency_links.txt
14
+ src/orgchat.egg-info/entry_points.txt
15
+ src/orgchat.egg-info/top_level.txt
16
+ tests/test_checker.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ orgchat = orgchat.cli:main
@@ -0,0 +1 @@
1
+ orgchat
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+ import tempfile
6
+ import unittest
7
+
8
+ from orgchat.checker import build_report
9
+ from orgchat.report import render_json, render_markdown
10
+
11
+
12
+ HEALTHY_CONFIG = f'''[project]
13
+ name = "Test Chatbot"
14
+
15
+ [data_sources]
16
+ sources = ["minio://docs"]
17
+ freshness_slo_minutes = 1440
18
+ sync_monitoring = true
19
+ owner = "Data team"
20
+ refresh_test = true
21
+
22
+ [access_control]
23
+ auth = "OIDC"
24
+ rbac = true
25
+ document_level_filters = true
26
+ audit_log = true
27
+ secret_scan = true
28
+
29
+ [quality]
30
+ eval_dataset = "evals/golden.jsonl"
31
+ automated_eval = true
32
+ regression_gate = true
33
+ min_score = 0.8
34
+ last_evaluation = "{date.today().isoformat()}"
35
+
36
+ [grounding]
37
+ citations = true
38
+ abstention = true
39
+ fallback = true
40
+ prompt_injection_defense = true
41
+ retrieval_logging = true
42
+
43
+ [observability]
44
+ tracing = true
45
+ token_cost_tracking = true
46
+ request_metrics = true
47
+ latency_slo_ms = 2000
48
+ dashboard = "ops/dashboard.json"
49
+
50
+ [monitoring]
51
+ healthcheck = "/health"
52
+ error_logging = true
53
+ alerts = true
54
+ backup_restore_test = true
55
+ incident_runbook = "docs/incident-runbook.md"
56
+
57
+ [ownership]
58
+ service_owner = "AI Platform"
59
+ technical_owner = "Tech lead"
60
+ oncall = "on-call"
61
+ runbook = "docs/runbook.md"
62
+ review_cadence = "monthly"
63
+ '''
64
+
65
+
66
+ class CheckerTests(unittest.TestCase):
67
+ def test_healthy_project_scores_pass(self) -> None:
68
+ with tempfile.TemporaryDirectory() as directory:
69
+ root = Path(directory)
70
+ (root / "evals").mkdir()
71
+ (root / "ops").mkdir()
72
+ (root / "docs").mkdir()
73
+ (root / "evals/golden.jsonl").write_text("{}\n", encoding="utf-8")
74
+ (root / "ops/dashboard.json").write_text("{}", encoding="utf-8")
75
+ (root / "docs/incident-runbook.md").write_text("# incidents", encoding="utf-8")
76
+ (root / "docs/runbook.md").write_text("# runbook", encoding="utf-8")
77
+ (root / "orgchat.toml").write_text(HEALTHY_CONFIG, encoding="utf-8")
78
+
79
+ report = build_report(root)
80
+
81
+ self.assertEqual(report.overall_status, "PASS")
82
+ self.assertEqual(report.score, 100.0)
83
+ self.assertIn("هر هفت محور", report.analysis)
84
+ self.assertTrue(all(check.status == "PASS" for check in report.checks))
85
+
86
+ def test_missing_configuration_is_actionable(self) -> None:
87
+ with tempfile.TemporaryDirectory() as directory:
88
+ report = build_report(directory)
89
+
90
+ self.assertEqual(report.overall_status, "WARN")
91
+ self.assertEqual(report.score, 0.0)
92
+ self.assertIn("orgchat.toml", report.analysis)
93
+ self.assertGreater(len(report.next_actions), 0)
94
+
95
+ def test_renderers_return_expected_formats(self) -> None:
96
+ with tempfile.TemporaryDirectory() as directory:
97
+ report = build_report(directory)
98
+ self.assertIn('"overall_status": "WARN"', render_json(report))
99
+ self.assertIn("# OrgChat Check", render_markdown(report))
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()