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>
This commit is contained in:
hamed
2026-08-01 14:37:32 +03:30
co-authored by Claude Opus 5
parent dedae05542
commit d98a0396a4
7 changed files with 99 additions and 19 deletions
@@ -72,7 +72,7 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
| ۳.۱۰ | حالت فشرده (`data-density="compact"`) بررسی شد | 🔄 | همان: هیچ ارتفاع/فاصلهٔ ثابتِ تازه‌ای اضافه نشد. بررسی چشمی در بازبینی پایانی |
| ۳.۱۱ | انتخاب چند سرویس با `SearchableSelect`؛ هیچ `<select>` بومی | ✅ | `ServiceSlotPicker` از `SearchableSelect` برای بخش و دکمه‌های toggle برای سرویس‌ها استفاده می‌کند (کد موجود). هیچ `<select>` بومی اضافه نشد |
| ۳.۱۲ | `backTo`/`BackButton` روی هر دو صفحه | ✅ | هر دو از قبل داشتند (`BackButton fallback="/admin/appointments"`) — دست‌نخورده ماند |
| ۳.۱۳ | وضعیت لیست رزروها در URL با `useUrlState` | | **به تعویق افتاد.** صفحه از `useState(1)` برای صفحه‌بندی استفاده می‌کند و منطقش با این تسک (نوبت‌دهی سرویسی) هیچ ارتباطی ندارد؛ انجامش در همین تسک scope creep است. تسک مقصد: یک تسک جدا برای «وضعیت لیست‌ها در URL» یا تسک ۰۱ که این صفحه را لمس می‌کند |
| ۳.۱۳ | وضعیت لیست رزروها در URL با `useUrlState` | | **به تعویق افتاد.** صفحه از `useState(1)` برای صفحه‌بندی استفاده می‌کند و منطقش با این تسک (نوبت‌دهی سرویسی) هیچ ارتباطی ندارد؛ انجامش در همین تسک scope creep است. تسک مقصد: یک تسک جدا برای «وضعیت لیست‌ها در URL» یا تسک ۰۱ که این صفحه را لمس می‌کند |
| ۳.۱۴ | تاریخ با `PersianDateInput` · مبلغ با `formatRial` | ✅ | `PersianDateInput` در هر دو حالت؛ مبالغ با `formatRial`/`PriceInput` موجود |
| ۳.۱۵ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ✅ | `marginInlineStart` استفاده شد نه `marginLeft`؛ ورودی‌های ساعت `dir="ltr"` مثل بقیهٔ پروژه |
| ۳.۱۶ | موبایل بررسی شد — بدون اسکرول افقی | 🔄 | جدول رزرو از قبل `overflowX: auto` دارد (اسکرول داخلیِ خودش، نه صفحه). بررسی چشمی در بازبینی پایانی |