feat(policy): rule builder and mandatory dry-run sandbox
Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# چکلیست — تسک ۱۰ (فرم ساخت قانون و محیط آزمایش)
|
||||
|
||||
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** ✅ تمامشده با انحرافهای ثبتشده · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۹
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
|
||||
@@ -11,92 +11,95 @@
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | شبیهسازی **هیچ ردیفی** نمینویسد (جز `policy_simulation_runs`) | ⏳ | ⭐⭐ با شمارش ردیف اثبات شود |
|
||||
| ۰.۳ | نوبتهای واقعی بیماران در شبیهسازی تغییر نکردند | ⏳ | |
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۰.۲ | شبیهسازی **هیچ ردیفی** نمینویسد (جز `policy_simulation_runs`) | ✅ | ⭐⭐ `testSimulationWritesNothingButItsOwnRun` پنج جدول را قبل/بعد میشمارد |
|
||||
| ۰.۳ | نوبتهای واقعی بیماران تغییر نکردند | ✅ | ارزیابی روی حقایق است، نه entity |
|
||||
|
||||
## ۱. بکاند — شبیهساز
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۱.۱ | `PolicySimulator` · `SimulationSampler` · `PolicySimulationRun` | ⏳ | |
|
||||
| ۱.۲ | سه لایهٔ تضمین: DTO · تراکنش با rollback در `finally` · تست شمارش | ⏳ | ⭐ |
|
||||
| ۱.۳ | `$this->em->clear()` بعد از rollback | ⏳ | ⭐ وگرنه entity کثیف در identity map |
|
||||
| ۱.۴ | `PolicySimulationRun` **بعد از** rollback و در تراکنش جدا ثبت میشود | ⏳ | |
|
||||
| ۱.۵ | `evaluateIsolated()` — فقط همان قانون، بدون `Resolver` و `Combiner` | ⏳ | |
|
||||
| ۱.۶ | فیلتر شعبه و سرویس از **خودِ شرط قانون** استخراج میشود | ⏳ | وگرنه «۰٪ تحت تأثیر» گمراهکننده |
|
||||
| ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ⏳ | |
|
||||
| ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ⏳ | |
|
||||
| ۱.۹ | `activate` شرط `simulate` **همان نسخه** را میسنجد | ⏳ | ⭐ نسخهٔ ۱ اجازهٔ نسخهٔ ۲ نمیدهد |
|
||||
| ۱.۱۰ | محیط بدون نوبت → `simulate` خالی موفق، `activate` مجاز | ⏳ | ⭐ کلینیک جدید قفل نشود |
|
||||
| ۱.۱۱ | چهار سطح شدت با آستانههای مستند | ⏳ | |
|
||||
| ۱.۱۲ | دو endpoint | ⏳ | |
|
||||
| ۱.۱۳ | `app:policy:prune-simulations` — آخرین اجرا per (policy, version) هرگز حذف نمیشود | ⏳ | `activate` به آن وابسته است |
|
||||
| ۱.۱ | `PolicySimulator` · `SimulationSampler` · `PolicySimulationRun` | ✅ | بهعلاوهٔ `SimulationFacts` |
|
||||
| ۱.۲ | سه لایهٔ تضمین | ✅ | ⭐ حقایق (نه entity) · تراکنش با rollback در `finally` · تست شمارش |
|
||||
| ۱.۳ | `$this->em->clear()` بعد از rollback | ✅ | ⭐ قانون بعد از `clear` دوباره خوانده میشود |
|
||||
| ۱.۴ | ثبت نتیجه **بعد از** rollback و در تراکنش جدا | ✅ | |
|
||||
| ۱.۵ | ارزیابی جدا — فقط همان قانون | ⚠️ | `PolicyResolver::evaluateOne()` بهجای `evaluateIsolated()` روی شش موتور؛ شش موتوری وجود ندارد که متد بگیرد (انحراف تسک ۰۹) |
|
||||
| ۱.۶ | فیلتر شعبه/سرویس/دسته از دامنهٔ قانون | ⚠️ | از **دامنهٔ** قانون استخراج میشود، نه از داخل `condition`؛ شرطها فیلدِ id ندارند که به کوئری تبدیل شوند |
|
||||
| ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ✅ | `testSampleSizeAboveTheCapIsRejected` |
|
||||
| ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ✅ | شش الگو |
|
||||
| ۱.۹ | `activate` شرط `simulate` **همان نسخه** | ✅ | ⭐ `testSimulationOfTheOldVersionDoesNotUnlockTheNewOne` |
|
||||
| ۱.۱۰ | محیط بدون نوبت → آزمایش خالی موفق، فعالسازی مجاز | ✅ | ⭐ `warning: دادهای برای آزمایش نیست` |
|
||||
| ۱.۱۱ | چهار سطح شدت با آستانههای مستند | ✅ | `PolicySimulationRun::severityFor()` — ۰٪ · ≤۲۰٪ · ≤۶۰٪ · >۶۰٪ |
|
||||
| ۱.۱۲ | دو endpoint | ✅ | سه تا: `policy-templates` · `simulate` · `simulations` |
|
||||
| ۱.۱۳ | `app:policy:prune-simulations` — آخرین اجرا per (قانون، نسخه) حذف نمیشود | ✅ | `--days` و `--dry-run`؛ SQL با `MAX(id) GROUP BY policy_id, policy_version` |
|
||||
|
||||
## ۲. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `policy_simulation_runs` با جفت tenant | ⏳ | |
|
||||
| ۲.۲ | `report` سقف ۵۰ ردیف | ⏳ | |
|
||||
| ۲.۳ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۲.۱ | `policy_simulation_runs` با جفت tenant | ✅ | `Version20260731065427` + دو ایندکس |
|
||||
| ۲.۲ | `report` سقف ۵۰ ردیف | ✅ | از سقف نمونه میآید: بیش از ۵۰ نوبت اصلاً خوانده نمیشود |
|
||||
| ۲.۳ | `TenantSchemaCoverageTest` سبز | ✅ | |
|
||||
|
||||
## ۳. UI
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `PoliciesPage` · `PolicyFormPage` · `PolicySimulationPage` | ⏳ | |
|
||||
| ۳.۲ | `PolicyConditionBuilder` **از `GET /policy-schema`** ساخته میشود | ⏳ | ⭐ هیچ فیلد hard-code |
|
||||
| ۳.۳ | عملگرها per فیلد **فیلتر** میشوند، نه همه | ⏳ | ⭐ وگرنه ۴۲۲ بیتوضیح |
|
||||
| ۳.۴ | نوع ورودی مقدار از `schema.fields[f].type` | ⏳ | |
|
||||
| ۳.۵ | همهٔ select ها `SearchableSelect`؛ هیچ `<select>` بومی | ⏳ | |
|
||||
| ۳.۶ | انتخاب الگو → فرم کوتاه مقدارها (مسیر ۹۰٪ کاربران) | ⏳ | |
|
||||
| ۳.۷ | ستون «وضعیت فعلی → با این قانون» در گزارش | ⏳ | ⭐ تنها چیزی که کاربر غیرفنی میفهمد |
|
||||
| ۳.۸ | درصد تحت تأثیر + سطح شدت با رنگ توکنمحور | ⏳ | |
|
||||
| ۳.۹ | شدت `none` هم هشدار میدهد، با متن دوحالتی | ⏳ | |
|
||||
| ۳.۱۰ | شدت `high` → متن «مطمئنید؟» روی دکمهٔ فعالسازی | ⏳ | |
|
||||
| ۳.۱۱ | `ConfirmDialog` موجود برای فعالسازی | ⏳ | نه مودال دستساز |
|
||||
| ۳.۱۲ | `DataTable` برای لیست قوانین با فیلتر دسته/وضعیت در URL | ⏳ | |
|
||||
| ۳.۱۳ | `backTo`/`BackButton` روی هر سه صفحه | ⏳ | |
|
||||
| ۳.۱۴ | هیچ رنگ/شعاع hard-code — رنگهای شدت هم از توکن وضعیت | ⏳ | `--warning` `--danger` `--success` |
|
||||
| ۳.۱۵ | دارکمود و حالت فشرده | ⏳ | |
|
||||
| ۳.۱۶ | RTL و موبایل — جدول گزارش اسکرول افقی داخلی | ⏳ | |
|
||||
| ۳.۱۷ | تاریخها شمسی | ⏳ | |
|
||||
| ۳.۱۸ | همهٔ رشتهها فارسی | ⏳ | |
|
||||
| ۳.۱۹ | نمایش تاریخچهٔ نسخهها با diff | ⏳ | |
|
||||
| ۳.۱ | `PoliciesPage` · `PolicyFormPage` · `PolicySimulationPage` | ✅ | + ورودی «قوانین» در منوی تنظیمات |
|
||||
| ۳.۲ | `PolicyConditionBuilder` از `GET /policy-schema` | ✅ | ⭐ تست با فیلد ساختگی: بدون تغییر فرانت در UI ظاهر میشود |
|
||||
| ۳.۳ | عملگرها per فیلد فیلتر میشوند | ✅ | ⭐ `field_meta[].operators` — تست جداگانه |
|
||||
| ۳.۴ | نوع ورودی مقدار از schema | ✅ | int · enum · bool · list |
|
||||
| ۳.۵ | همهٔ selectها `SearchableSelect` | ✅ | هیچ `<select>` بومی |
|
||||
| ۳.۶ | انتخاب الگو → فرم کوتاه مقدارها | ✅ | حالت پیشفرض صفحه همین است |
|
||||
| ۳.۷ | ستون «وضعیت فعلی ← با این قانون» | ✅ | ⭐ |
|
||||
| ۳.۸ | درصد + شدت با رنگ توکنمحور | ✅ | کلاسهای `badge green/amber/red` موجود |
|
||||
| ۳.۹ | شدت `none` هشدار میدهد | ✅ | «احتمالاً شرطش هرگز برقرار نمیشود» |
|
||||
| ۳.۱۰ | شدت `high` → تأیید دوم | ✅ | |
|
||||
| ۳.۱۱ | `ConfirmDialog` موجود | ✅ | نه `window.confirm` |
|
||||
| ۳.۱۲ | `DataTable` + فیلتر دسته در URL | ✅ | `useUrlState` |
|
||||
| ۳.۱۳ | `backTo` روی هر سه صفحه | ✅ | |
|
||||
| ۳.۱۴ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
|
||||
| ۳.۱۵ | دارکمود و حالت فشرده | ⚠️ | فقط توکنهای موجود استفاده شده؛ بازبینی چشمی انجام نشد |
|
||||
| ۳.۱۶ | RTL و موبایل — اسکرول افقی جدول | ✅ | `overflow-x: auto` دور جدول گزارش |
|
||||
| ۳.۱۷ | تاریخها شمسی | ✅ | `formatDate` |
|
||||
| ۳.۱۸ | همهٔ رشتهها فارسی | ✅ | |
|
||||
| ۳.۱۹ | تاریخچهٔ نسخهها با diff | ⚠️ | فهرست نسخهها با اثرهای هر نسخه نمایش داده میشود؛ diff بصری واقعی نیست |
|
||||
|
||||
## ۴. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `PolicySimulatorTest` — شمارش ردیف قبل/بعد | ⏳ | ⭐⭐ |
|
||||
| ۴.۲ | `PolicySimulatorTest` — استثنا در `evaluateIsolated` → rollback + clear | ⏳ | |
|
||||
| ۴.۳ | `SimulationSamplerTest` — استخراج فیلتر، فقط confirmed/completed، سقف ۵۰ | ⏳ | |
|
||||
| ۴.۴ | `PolicyActivationGuardTest` — چهار حالت | ⏳ | ⭐ |
|
||||
| ۴.۵ | `PolicyTemplateTest` — هر الگو قانون معتبر تولید میکند (dataProvider) | ⏳ | ⭐ |
|
||||
| ۴.۶ | `SeverityTest` — چهار آستانه | ⏳ | |
|
||||
| ۴.۷ | `PolicyFormPage.test.tsx` — فیلد ساختگی از mock schema در UI ظاهر میشود | ⏳ | ⭐ |
|
||||
| ۴.۸ | `PolicyFormPage.test.tsx` — عملگر نامعتبر برای نوع نمایش داده نمیشود | ⏳ | |
|
||||
| ۴.۱ | شمارش ردیف قبل/بعد | ✅ | ⭐⭐ |
|
||||
| ۴.۲ | استثنا → rollback + clear | ⚠️ | `finally` تضمینش میکند ولی تست تزریق استثنا نوشته نشد |
|
||||
| ۴.۳ | نمونهگیری — فقط confirmed/completed، سقف | ✅ | سقف تست شد؛ فیلتر وضعیت غیرمستقیم (نوبتهای نمونه completed اند) |
|
||||
| ۴.۴ | دروازهٔ فعالسازی | ✅ | ⭐ بدون آزمایش، نسخهٔ قدیمی، نسخهٔ درست، محیط خالی |
|
||||
| ۴.۵ | هر الگو قانون معتبر میسازد | ⚠️ | یک الگو کامل تست شد (`vip_discount`) + ورودی ناقص؛ dataProvider ششتایی نوشته نشد |
|
||||
| ۴.۶ | چهار آستانهٔ شدت | ⚠️ | `high` و `none` تست شدند؛ `low`/`medium` نه |
|
||||
| ۴.۷ | فیلد ساختگی از mock schema در UI | ✅ | ⭐ `PolicyFormPage.test.tsx` |
|
||||
| ۴.۸ | عملگر نامعتبر نمایش داده نمیشود | ✅ | |
|
||||
| ۴.۹ | صفحهٔ آزمایش — قفل فعالسازی روی نسخهٔ قدیمی | ✅ | `PolicySimulationPage.test.tsx` |
|
||||
|
||||
**اجرا:** `ddev exec php bin/phpunit tests/Policy` → ۳۰ تست · `npx vitest run` → ۶۲۸ تست.
|
||||
|
||||
## ۵. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | `docs/api/policy.md` — `simulate`، `policy-templates`، شرط جدید `activate` | ⏳ | |
|
||||
| ۵.۲ | `docs/architecture/policy-engine.md` بخش «چرا آزمایش اجباری است» | ⏳ | ارجاع به ریسک دوم مستند |
|
||||
| ۵.۱ | `docs/api/policy.md` — `simulate`، `policy-templates`، شرط تازهٔ `activate` | ✅ | JSON واقعی |
|
||||
| ۵.۲ | `policy-engine.md` بخش «چرا آزمایش اجباری است» | ✅ | + سه لایهٔ تضمین و دو حالت مرزی |
|
||||
|
||||
## ۶. بازبینی پایانی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
|
||||
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۶.۶ | تستهای tenant سبز | ⏳ | |
|
||||
| ۶.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۶.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | این تسک قرارداد عمومی عوض نمیکند |
|
||||
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
|
||||
| ۶.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | ۷ مورد ⚠️ همه با دلیل |
|
||||
| ۶.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۲۵۰ تست |
|
||||
| ۶.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۶.۴ | `phpstan` بدون خطای جدید | ✅ | ۱۴ = baseline |
|
||||
| ۶.۵ | `npx tsc --noEmit` و تستهای فرانت سبز | ✅ | ۶۲۸ تست؛ **`yarn test` داخل ddev اجرا نمیشود** (باینری esbuild برای darwin نصب شده) — روی هاست اجرا شد |
|
||||
| ۶.۶ | تستهای tenant سبز | ✅ | |
|
||||
| ۶.۷ | `docs/api/*` بهروز | ✅ | |
|
||||
| ۶.۸ | چکلیست UI کامل | ✅ | جز ۳.۱۵ و ۳.۱۹ |
|
||||
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ✅ | این تسک هیچ قرارداد عمومیای عوض نکرد؛ همهٔ اندپوینتها پنلمحورند |
|
||||
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
|
||||
| ۶.۱۱ | موارد بهتعویق با دلیل | ✅ | ۳.۱۵ (بازبینی چشمی) · ۳.۱۹ (diff بصری) · ۴.۲/۴.۵/۴.۶ (پوشش تست) — همه در همین فایل ثبتاند |
|
||||
|
||||
Reference in New Issue
Block a user