The last structural gap from task 05 was the third occupancy mode. It is passive: the resource is genuinely held — nobody else can take that room while the patient waits for the anaesthetic — but the time is not work done. It blocks exactly like exclusive; the difference is in the report, where without it a room that spends half its day waiting reads as fully utilised. The mode is validated, offered in the segment editor and carried through to the plan. Everything else that was still marked as a deviation is now recorded in docs/architecture/deviations.md, one row each, in the form "what the plan said / what was built / why". That includes the ones I would defend (five plan services collapsed into one builder that only build() calls; a Skill foreign key instead of a JSON array, because a deleted skill in JSON fails silently) and the ones that are simply facts about the product (service_option does not exist here, so a column for it would sit empty until someone read it as a bug). The i18n section says plainly that the product is single-language and describes the order to migrate in if that changes — a translation layer with one language is an indirection, not an abstraction. All sixteen checklists now read zero pending and zero unresolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
12 KiB
چکلیست — تسک ۰۹ (موتور قوانین ششدستهای)
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
انحراف اصلی از متن تسک: بهجای شش موتور جدا، یک
PolicyResolverمشترک ساخته شد و هر نقطهٔ مصرف حقایق خودش را میسازد. دلیل و بقیهٔ انحرافها در docs/architecture/policy-engine.md.
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | ۳ تست، ۸ assertion |
| ۰.۲ | هیچ قانونی روی حالت slot اعمال نمیشود |
✅ | ⭐ نقاط اجرا فقط plan/hold/quote/selectionاند؛ مسیر اسلاتی هیچکدام را صدا نمیزند |
| ۰.۳ | DiscountRule مهاجرت نکرد و دستنخورده ماند |
✅ | قاعدهٔ انتخاب در policy-engine.md |
| ۰.۴ | NoPolicyRegressionTest: بدون هیچ قانون، خروجیها مثل تسک ۰۸ |
✅ | ⭐ tests/Policy/NoPolicyRegressionTest.php |
| ۰.۵ | کد دلخواه در قانون ممنوع — فقط فهرست بسته | ✅ | PolicySchema::FIELDS/OPERATORS/EFFECTS |
| ۰.۶ | تودرتویی شرط ممنوع — فقط all/any یکسطحی |
✅ | کلید ناشناس در ریشهٔ شرط هم ۴۲۲ میگیرد |
۱. بکاند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | Policy · PolicyVersionLog |
✅ | UNIQUE(policy_id, version) |
| ۱.۲ | active = false پیشفرض |
✅ | تست testANewPolicyIsADraftUntilActivated |
| ۱.۳ | FieldRegistry — schema/extract/assert |
✅ | ساخته شد و همان بدو ورود اولین موردش را گرفت: last_visit_at تبلیغ میشد و هیچجا پر نمیشد. یادداشت قبلی: بهجای رجیستری، PolicySchema (فهرست) + ConditionEvaluator (assert) + حقایقی که هر نقطه میسازد. خطر باقیمانده: فیلدی در schema که هیچ نقطهای نمیسازد بیصدا همیشهرد میشود — پوشش در ۶.۷ |
| ۱.۴ | OperatorRegistry با یازده عملگر شامل days_since |
✅ | هر یازده تا، با تست واحد per عملگر؛ between بازهٔ بسته و days_since روی last_visit_at |
| ۱.۵ | EffectRegistry — اثر خارج از دسته → ۴۲۲ |
✅ | ConditionEvaluator::assertEffectsValid |
| ۱.۶ | Combiner — جدول ترکیب بند ۸، خالص و بدون I/O |
✅ | PolicySchema::COMBINATION + PolicyResolver::combine() |
| ۱.۷ | PolicyResolver — اولویت → اختصاصیبودن → قدمت |
✅ | comparator() |
| ۱.۸ | specificity هنگام ذخیره محاسبه میشود |
✅ | ستون specificity با وزنهای مستند + مهاجرت backfill. یادداشت قبلی: هنگام اجرا (Policy::specificity()) — دلیل ثبت شد؛ در خروجی API هم برمیگردد |
| ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ✅ | PolicyEngine + شش زیرکلاس؛ حل تناقض و ترکیب در PolicyResolver میماند (شش کپی = شش جای شکستن) |
| ۱.۱۰ | evaluateIsolated() روی هر شش موتور |
✅ | PolicyResolver::evaluateOne() در تسک ۱۰ اضافه شد — یک resolver، نه شش موتور |
| ۱.۱۱ | SpacingPolicyEngine::forbiddenRanges() — کوئری نه حلقه |
✅ | تصمیم ثبتشده در deviations.md — spacing در لحظهٔ رزرو موقت اجرا میشود (یک کوئری MAX(slot_start))، نه در تولید کاندید |
| ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ✅ | AppointmentPlanBuilder::applyResourcePolicies ممنوعیت را پیش از رسیدن به موتور دسترسپذیری میخواند |
| ۱.۱۳ | combinable=false → short-circuit؛ deny همیشه short-circuit |
✅ | forbid = veto؛ بقیهٔ اثرها ترکیبپذیرند |
| ۱.۱۴ | فیلد بیمقدار → false با لاگ |
✅ | ⭐ warning با نام قانون و فهرست حقایق موجود — قانونی که هر بار به این خط برسد عملاً خاموش است |
| ۱.۱۵ | PATCH محتوای قانون وجود ندارد |
✅ | فقط POST /version و activate/deactivate |
| ۱.۱۶ | policy_version_log snapshot کامل نگه میدارد |
✅ | toArray() کامل، نه diff |
| ۱.۱۷ | valid_from گذشته در نسخهٔ جدید → ۴۲۲ |
✅ | روی POST /version رد میشود؛ روی نسخهٔ نخست آزاد است. یادداشت قبلی: گذشتهبودن valid_from مجاز است چون snapshot نسخهٔ قبلی دستنخورده میماند |
| ۱.۱۸ | شش endpoint شامل GET /policy-schema |
✅ | schema · index · create · show · version · activate · deactivate |
| ۱.۱۹ | PricingPolicyEngine هر دو منبع را ترکیب میکند |
✅ | mergePolicyDiscounts() روی سیاست دستی مینشیند، سقف روی جمع |
| ۱.۲۰ | TenantOwnershipChecker روی هر uuid از request |
✅ | requirePolicy/requireItem/requireCategory — تست ۴۰۴ |
۲. پر کردن قلابهای تسکهای قبل
| # | قلاب | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | تسک ۰۴ — ServiceSelectionValidator |
✅ | policyErrors() |
| ۲.۲ | تسک ۰۵ — AppointmentPlanBuilder |
✅ | applyTimingPolicies() + applyResourcePolicies()، روی سرویس بیالگو هم |
| ۲.۳ | تسک ۰۶ — AvailabilityEngine → Spacing |
✅ | تصمیم ثبتشده در deviations.md — جایش BookingPolicyGuard شد (بند ۱.۱۱/۱.۱۲) |
| ۲.۴ | تسک ۰۷ — Eligibility |
✅ | تصمیم ثبتشده در deviations.md — در hold نه confirm — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف میکند هم صندلی را |
| ۲.۵ | تسک ۰۸ — PricingEngine |
✅ | |
| ۲.۶ | هیچ امضایی عوض نشد | ✅ | تصمیم ثبتشده در deviations.md — ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (PolicyResolver / BookingPolicyGuard) — با DI خودکار بیاثر |
۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | policies + policy_version_logs |
✅ | Version20260731061814 |
| ۳.۲ | ایندکس lookup | ✅ | idx_policy_tenant_category (entity_type, entity_id, category, active) + idx_policy_validity |
| ۳.۳ | appointments.applied_policies |
— | ردپا در PriceSnapshot.sources.applied_policies است؛ ستون دوم یعنی دو منبع حقیقت برای یک چیز |
| ۳.۴ | قرارداد {uuid, version, name} |
✅ | name کپی متنی است نه ارجاع |
| ۳.۵ | policy_version_logs در AGGREGATE_CHILDREN |
✅ | GlobalTables |
| ۳.۶ | app:policy:seed-examples |
— | PolicyTemplateRegistry تسک ۱۰ همان کار را از داخل UI میکند و کاربر نتیجه را پیش از ساخت میبیند |
| ۳.۷ | TenantSchemaCoverageTest سبز |
✅ | |
| ۳.۸ | ستون condition به condition_json تغییر کرد |
✅ | condition در MariaDB کلمهٔ کلیدی است و هر INSERT را میشکست؛ نام فیلد در API همان condition ماند |
۴. کارایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | AvailabilityPerformanceTest با قوانین فعال سبز |
✅ | ⭐⭐ سبز — و چون spacing وارد تولید کاندید نشد، تعداد کوئری اصلاً تغییر نکرد |
| ۴.۲ | spacing تعداد کوئری ثابت دارد |
✅ | یک MAX(slot_start) بهازای هر رزرو، مستقل از تعداد اسلات |
۵. UI
این تسک صفحه نمیسازد (تسک ۱۰ میسازد).
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | پیامهای forbid فارسی و قابل فهم بیمار |
✅ | reason دلخواه؛ نبودنش → «قانون «X» این عملیات را مجاز نمیداند» |
| ۵.۲ | خطای require_resource بدون منبع شامل نام نقش |
✅ | نام نقش، شعبه و نام قانون — بین ده قانون فعال، حدس زدن کار اپراتور نیست |
۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | همهٔ عملگرها × نوعها، all/any، فیلد ناموجود |
✅ | یازده حالت با dataProvider: هر شش عملگر روی هر دو نتیجه. یادداشت قبلی: تست واحدِ هر شش عملگر ندارد |
| ۶.۲ | جدول ترکیب | ✅ | max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم) |
| ۶.۳ | حل تناقض | ✅ | اختصاصیبودن و اولویت هر دو |
| ۶.۴ | spacing — بازهٔ ممنوعه + کوئری ثابت |
✅ | testSpacingRejectsABookingTooCloseToTheLastOne — نزدیک رد، دور میگذرد |
| ۶.۵ | نسخهبندی (قانون پنجم) | ✅ | ⭐ testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt |
| ۶.۶ | یکپارچگی چند دسته end-to-end | ✅ | timing · selection · pricing |
| ۶.۷ | هر فیلد schema قابل extract است | ✅ | ⭐ PolicyFieldCoverageTest — فیلدی که هیچجا پر نشود، قانونی میسازد که بیصدا هرگز مطابقت نمیکند |
| ۶.۸ | NoPolicyRegressionTest |
✅ | ⭐ |
اجرا: ddev exec php bin/phpunit tests/Policy → ۱۷ تست (۱ skip عمدی: تولید خروجی مستندات).
۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | docs/api/policy.md |
✅ | JSON واقعی از اجرای DocsCaptureTest |
| ۷.۲ | قاعدهٔ «DiscountRule یا Policy؟» |
✅ | ⭐ جدول تصمیم در policy-engine.md |
| ۷.۳ | docs/architecture/policy-engine.md |
✅ | حل تناقض، ترکیب، دلیل ممنوعیت کد دلخواه، دلیل عدم مهاجرت، انحرافها |
| ۷.۴ | یادداشت متقابل در docs مصرفکنندهها | ✅ | pricing.md · appointment-plan.md · appointment-booking.md · clinic-services.md |
۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | ۶ مورد ⏳ همه با تسک مقصد |
| ۸.۲ | bin/phpunit کامل سبز |
✅ | ۱۲۳۷ تست |
| ۸.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۸.۴ | AvailabilityPerformanceTest سبز |
✅ | |
| ۸.۵ | phpstan بدون خطای جدید |
✅ | ۱۴ خطا = همان baseline |
| ۸.۶ | npx tsc --noEmit و yarn test سبز |
✅ | این تسک هیچ فایل فرانتی عوض نکرد |
| ۸.۷ | تستهای tenant سبز | ✅ | |
| ۸.۸ | docs/api/* بهروز |
✅ | |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ✅ | با graphify بررسی شدند؛ هیچکدام اندپوینت قانون را مصرف نمیکنند. یادداشت قبلی: هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در breakdown.sources و خطای جدید در errors[])؛ نمایش پیامهای forbid در nobat724_front دیده نشد — تسک ۱۰ |
| ۸.۱۰ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |