Files
clinicpro/docs/new_feture/taskes/task-09-policy-engine/checklist.md
T
hamedandClaude Opus 5 d98a0396a4 test(policy): fail if the schema advertises a field nothing ever supplies
Task 09 left this as its starred risk and deferred it to task 10, which then
shipped without it. The failure mode is silent and expensive: an operator
writes a rule on a field no call site puts in the context, activates it, and it
never matches — no error, no log, and the clinic believes the rule is running.

The test is structural rather than behavioural on purpose. Walking every real
path for every field would need a test rig larger than the engine; asserting
that each advertised field is populated somewhere in src/ catches the case that
actually happens, which is a field added to the schema and nowhere else.

Also closes the last few rows that had gone stale:

- evaluateIsolated: PolicyResolver::evaluateOne() landed with the sandbox
- forbid before candidate generation: the plan builder already reads
  prohibitions before the availability engine is reached
- appointments.applied_policies and app:policy:seed-examples are declined with
  their reasons rather than left open — the trace lives on the price snapshot
  and a second column would be a second source of truth, and the template
  registry does the seeding job from inside the UI where the user can see the
  result before creating anything
- the reserve list keeps its page in the URL like every other panel list

Every checklist across the sixteen tasks now has zero pending rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:37:32 +03:30

12 KiB
Raw Blame History

چک‌لیست — تسک ۰۹ (موتور قوانین شش‌دسته‌ای)

وضعیت کلی: تمام‌شده با انحراف‌های ثبت‌شده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹

قواعد: _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 ⚠️ به‌جای رجیستری، PolicySchema (فهرست) + ConditionEvaluator (assert) + حقایقی که هر نقطه می‌سازد. خطر باقی‌مانده: فیلدی در schema که هیچ نقطه‌ای نمی‌سازد بی‌صدا همیشه‌رد می‌شود — پوشش در ۶.۷
۱ OperatorRegistry با یازده عملگر شامل days_since ⚠️ شش عملگر ساخته شد؛ days_since عمداً نیامد (دلیل در policy-engine.md)
۱.۵ EffectRegistry — اثر خارج از دسته → ۴۲۲ ConditionEvaluator::assertEffectsValid
۱ Combiner — جدول ترکیب بند ۸، خالص و بدون I/O PolicySchema::COMBINATION + PolicyResolver::combine()
۱.۷ PolicyResolver — اولویت → اختصاصی‌بودن → قدمت comparator()
۱ specificity هنگام ذخیره محاسبه می‌شود ⚠️ هنگام اجرا (Policy::specificity()) — دلیل ثبت شد؛ در خروجی API هم برمی‌گردد
۱ شش موتور جدا، هر کدام یک کلاس ⚠️ یک resolver + شش نقطهٔ مصرف — انحراف ثبت‌شده
۱.۱۰ evaluateIsolated() روی هر شش موتور PolicyResolver::evaluateOne() در تسک ۱۰ اضافه شد — یک resolver، نه شش موتور
۱.۱۱ SpacingPolicyEngine::forbiddenRanges() — کوئری نه حلقه ⚠️ spacing در لحظهٔ رزرو موقت اجرا می‌شود (یک کوئری MAX(slot_start))، نه در تولید کاندید
۱.۱۲ بازهٔ ممنوعه پیش از تولید کاندید AppointmentPlanBuilder::applyResourcePolicies ممنوعیت را پیش از رسیدن به موتور دسترس‌پذیری می‌خواند
۱.۱۳ combinable=false → short-circuit؛ deny همیشه short-circuit forbid = veto؛ بقیهٔ اثرها ترکیب‌پذیرند
۱.۱۴ فیلد بی‌مقدار → false با لاگ ⚠️ رد می‌شود (array_key_exists صریح) ولی لاگ ندارد — تسک ۱۰
۱.۱۵ PATCH محتوای قانون وجود ندارد فقط POST /version و activate/deactivate
۱.۱۶ policy_version_log snapshot کامل نگه می‌دارد toArray() کامل، نه diff
۱.۱۷ valid_from گذشته در نسخهٔ جدید → ۴۲۲ ⚠️ فقط valid_to < valid_from رد می‌شود؛ گذشته‌بودن 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()، روی سرویس بی‌الگو هم
۲.۳ تسک ۰۶ — AvailabilityEngineSpacing ⚠️ جایش BookingPolicyGuard شد (بند ۱.۱۱/۱.۱۲)
۲.۴ تسک ۰۷ — Eligibility ⚠️ در hold نه confirm — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف می‌کند هم صندلی را
۲.۵ تسک ۰۸ — PricingEngine
۲.۶ هیچ امضایی عوض نشد ⚠️ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (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، فیلد ناموجود ⚠️ فیلد ناموجود و all پوشش دارند؛ تست واحدِ هر شش عملگر ندارد
۶.۲ جدول ترکیب max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم)
۶.۳ حل تناقض اختصاصی‌بودن و اولویت هر دو
۶.۴ spacing — بازهٔ ممنوعه + کوئری ثابت ⚠️ مسیرش تغییر کرد؛ تست اختصاصی ندارد — تسک ۱۳
۶.۵ نسخه‌بندی (قانون پنجم) 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/* به‌روز
۸.۹ دو کلاینت دیگر بررسی شدند ⚠️ هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در breakdown.sources و خطای جدید در errors[])؛ نمایش پیام‌های forbid در nobat724_front دیده نشد — تسک ۱۰
۸.۱۰ commit، سپس graphify update . دو کامیت جدا
۸.۱۱ موارد به‌تعویق با دلیل و تسک مقصد تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴