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 +114 -0
- orgchat-0.1.0/README.md +105 -0
- orgchat-0.1.0/pyproject.toml +19 -0
- orgchat-0.1.0/setup.cfg +4 -0
- orgchat-0.1.0/src/orgchat/__init__.py +3 -0
- orgchat-0.1.0/src/orgchat/__main__.py +5 -0
- orgchat-0.1.0/src/orgchat/checker.py +341 -0
- orgchat-0.1.0/src/orgchat/cli.py +85 -0
- orgchat-0.1.0/src/orgchat/config.py +47 -0
- orgchat-0.1.0/src/orgchat/models.py +73 -0
- orgchat-0.1.0/src/orgchat/report.py +84 -0
- orgchat-0.1.0/src/orgchat/templates.py +56 -0
- orgchat-0.1.0/src/orgchat.egg-info/PKG-INFO +114 -0
- orgchat-0.1.0/src/orgchat.egg-info/SOURCES.txt +16 -0
- orgchat-0.1.0/src/orgchat.egg-info/dependency_links.txt +1 -0
- orgchat-0.1.0/src/orgchat.egg-info/entry_points.txt +2 -0
- orgchat-0.1.0/src/orgchat.egg-info/top_level.txt +1 -0
- orgchat-0.1.0/tests/test_checker.py +103 -0
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
|
+
این نسخه وضعیت آمادگی عملیاتی را گزارش میکند و جایگزین تست امنیتی، ارزیابی مدل یا ممیزی سازمانی کامل نیست.
|
orgchat-0.1.0/README.md
ADDED
|
@@ -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"]
|
orgchat-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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()
|