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:
@@ -100,6 +100,39 @@
|
||||
|
||||
---
|
||||
|
||||
## چرا آزمایش اجباری است
|
||||
|
||||
بند ۱۷ مستند، ریسک دوم: «کاربر غیرفنی نمیتواند قانون درست تعریف کند → قانونهای اشتباه،
|
||||
رفتار عجیب». موتور قانون بدون آزمایشگاه یک API قدرتمند است که هیچکس نمیتواند درست از
|
||||
آن استفاده کند.
|
||||
|
||||
پس `activate` یک شرط دارد: یک اجرای آزمایشیِ **همین نسخه** باید ثبت شده باشد. آزمایش
|
||||
قانون را روی نوبتهای واقعیِ گذشته اجرا میکند و میگوید چند نوبت تغییر میکردند و دقیقاً
|
||||
چه تغییری. عددِ «۷۵٪ نوبتها رد میشدند» چیزی است که کاربر غیرفنی هم میفهمد.
|
||||
|
||||
نسخهمحور بودن شرط عمدی است: کاربری که گزارش را دید و بعد متن قانون را عوض کرد، دیگر
|
||||
گزارشی از قانونِ فعلی ندارد.
|
||||
|
||||
### هیچ چیز ثبت نمیشود — سه لایه
|
||||
|
||||
۱. ارزیابی روی **حقایق** انجام میشود نه روی entity؛ هیچ entity ای تغییر نمیکند.
|
||||
۲. کل اجرا در تراکنشی است که در `finally` همیشه `rollback` و `clear` میشود. `clear`
|
||||
اختیاری نیست: entity های لمسشده در identity map میمانند و اولین `flush` بعدی در
|
||||
همان request ثبتشان میکند — باگی که پیدا کردنش روزها میبرد.
|
||||
۳. `PolicySimulationTest::testSimulationWritesNothingButItsOwnRun` تعداد ردیف جدولهای
|
||||
حساس را قبل و بعد میشمارد.
|
||||
|
||||
خودِ `PolicySimulationRun` **بعد** از این بلوک و در تراکنش خودش ثبت میشود.
|
||||
|
||||
### دو حالتِ مرزی که عمداً موفقاند
|
||||
|
||||
- **محیط بدون نوبت گذشته** → گزارش خالی با `warning`. اگر خطا بود، کلینیک تازه هرگز
|
||||
نمیتوانست قانونی فعال کند.
|
||||
- **قانونی که هیچ نوبتی را تغییر نمیدهد** → موفق ولی با شدت `none`، که خودش هشدار
|
||||
است: شرط احتمالاً هرگز برقرار نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## تصمیمهای ثبتشده و انحرافها
|
||||
|
||||
| موضوع | تصمیم | دلیل |
|
||||
@@ -107,5 +140,6 @@
|
||||
| یک `PolicyResolver` بهجای شش موتور جدا | یک resolver + یک نقطهٔ اجرا در هر سرویس مقصد | شش کلاس با همان بدنه فقط تکرار بود؛ تفاوت واقعی در حقایق است که هر نقطه خودش میسازد |
|
||||
| `spacing` در لحظهٔ رزرو موقت، نه در تولید کاندید | رد کردن هنگام `hold` | نگه داشتن تعداد کوئریِ `AvailabilityEngine` ثابت؛ **هزینهاش** این است که اسلات نمایش داده میشود و بعد رد؛ بستنِ آن در تولید کاندید به تسک ۱۳ موکول شد |
|
||||
| `specificity` هنگام اجرا حساب میشود | متد `Policy::specificity()` | ستون ذخیرهشده باید با تغییر دامنه همزمان بهروز بماند؛ محاسبهٔ درجا سه مقایسهٔ صحیح است |
|
||||
| `appointments.applied_policies` ساخته نشد | فعلاً `PriceSnapshot.sources.applied_policies` | نوبتهای بدون فاکتور هنوز ردپای قانون ندارند — تسک ۱۰ |
|
||||
| `appointments.applied_policies` ساخته نشد | فعلاً `PriceSnapshot.sources.applied_policies` | نوبتهای بدون فاکتور هنوز ردپای قانون ندارند — تسک ۱۴ (رویدادها) |
|
||||
| یک `PolicyResolver::evaluateOne()` بهجای `evaluateIsolated()` روی شش موتور | همان resolver، بدون رقابت و ترکیب | شش موتور جدایی وجود ندارد که متد بگیرد؛ رفتار همان است |
|
||||
| عملگر `days_since` اضافه نشد | `min_days_between` مستقیم فاصله را میسنجد | تنها مصرفش همان دستهٔ `spacing` بود؛ عملگری که یک مصرف دارد، اثر است نه عملگر |
|
||||
|
||||
Reference in New Issue
Block a user