feat(policy): six-category policy engine wired into the booking flow
Rules become data instead of code: a clinic can say "laser under 18 requires parental consent" without a deploy. Engine - Policy / PolicyVersionLog entities, closed field/operator/effect lists per category (PolicySchema), condition validation at write time - PolicyResolver: priority -> specificity -> age, combining effects by veto / max / sum / union - A missing fact fails its clause instead of silently passing it - Policies are drafts until activated, and are versioned rather than edited Wiring - selection -> ServiceSelectionValidator - eligibility + spacing -> BookingPolicyGuard, at hold time not confirm time - resource + timing -> AppointmentPlanBuilder, including template-less services - pricing -> PricingEngine, alongside (not replacing) the manual discount The condition column is named condition_json: `condition` is a MariaDB keyword and broke every INSERT. Tests: 17 in tests/Policy including NoPolicyRegressionTest, which pins that a clinic with no policies sees byte-identical output to task 08. Docs: docs/api/policy.md (real captured JSON) + docs/architecture/policy-engine.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,120 +1,128 @@
|
||||
# چکلیست — تسک ۰۹ (موتور قوانین ششدستهای)
|
||||
|
||||
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** ✅ تمامشده با انحرافهای ثبتشده · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۹
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
|
||||
|
||||
> **انحراف اصلی از متن تسک:** بهجای شش موتور جدا، یک `PolicyResolver` مشترک ساخته شد و
|
||||
> هر نقطهٔ مصرف حقایق خودش را میسازد. دلیل و بقیهٔ انحرافها در
|
||||
> [docs/architecture/policy-engine.md](../../../architecture/policy-engine.md#تصمیمهای-ثبتشده-و-انحرافها).
|
||||
|
||||
---
|
||||
|
||||
## ۰. خط سرخ
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | **هیچ قانونی روی حالت `slot` اعمال نمیشود** | ⏳ | ⭐ حتی اگر منطقی به نظر برسد |
|
||||
| ۰.۳ | `DiscountRule` مهاجرت نکرد و دستنخورده ماند | ⏳ | |
|
||||
| ۰.۴ | `NoPolicyRegressionTest`: بدون هیچ قانون، خروجیها بیتبهبیت مثل تسک ۰۸ | ⏳ | ⭐ |
|
||||
| ۰.۵ | کد دلخواه در قانون **ممنوع** — فقط فهرست بسته | ⏳ | مستند بند ۸ |
|
||||
| ۰.۶ | تودرتویی شرط ممنوع — فقط `all`/`any` یکسطحی | ⏳ | |
|
||||
| ۰.۱ | `--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` | ⏳ | |
|
||||
| ۱.۲ | `active = false` پیشفرض | ⏳ | تسک ۱۰ آزمایش را اجبار میکند |
|
||||
| ۱.۳ | `FieldRegistry` — سه مسئولیت روی یک آرایه (schema/extract/assert) | ⏳ | ⭐ فیلد نمایشیِ بیارزیابی ممکن نشود |
|
||||
| ۱.۴ | `OperatorRegistry` با یازده عملگر شامل `days_since` | ⏳ | |
|
||||
| ۱.۵ | `EffectRegistry` — اثر خارج از دسته → ۴۲۲ | ⏳ | |
|
||||
| ۱.۶ | `Combiner` — جدول ترکیب مستند بند ۸، خالص و بدون I/O | ⏳ | |
|
||||
| ۱.۷ | `PolicyResolver` — اولویت → اختصاصیبودن → قدمت | ⏳ | |
|
||||
| ۱.۸ | `specificity` هنگام **ذخیره** محاسبه میشود، نه اجرا | ⏳ | |
|
||||
| ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ⏳ | نه یک `PolicyEngine` بزرگ |
|
||||
| ۱.۱۰ | `evaluateIsolated()` روی هر شش موتور | ⏳ | ⭐ تسک ۱۰ به آن نیاز دارد — اینجا اضافه شود |
|
||||
| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری، **نه حلقه per slot** | ⏳ | ⭐ |
|
||||
| ۱.۱۲ | بازهٔ ممنوعه **پیش از** تولید کاندید به `CandidateGenerator` میرود | ⏳ | نه فیلتر بعدی |
|
||||
| ۱.۱۳ | `combinable=false` → short-circuit؛ `deny` همیشه short-circuit | ⏳ | |
|
||||
| ۱.۱۴ | فیلد بیمقدار → `false` **با لاگ**، نه سکوت | ⏳ | ⭐ قانون خاموش بیصدا |
|
||||
| ۱.۱۵ | `PATCH` محتوای قانون وجود ندارد؛ فقط `name` و `active` | ⏳ | نسخهبندی |
|
||||
| ۱.۱۶ | `policy_version_log` snapshot **کامل** نگه میدارد، نه diff | ⏳ | |
|
||||
| ۱.۱۷ | `valid_from` گذشته در نسخهٔ جدید → ۴۲۲ | ⏳ | قانون پنجم |
|
||||
| ۱.۱۸ | شش endpoint شامل `GET /policy-schema` | ⏳ | |
|
||||
| ۱.۱۹ | `PricingPolicyEngine` هر دو منبع (`DiscountRule` + `Policy`) را ترکیب میکند | ⏳ | |
|
||||
| ۱.۲۰ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
|
||||
| ۱.۱ | `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::resolve()` بدون I/O جانبی است، پس تسک ۱۰ میتواند مستقیم صدایش بزند |
|
||||
| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری نه حلقه | ⚠️ | `spacing` در لحظهٔ رزرو موقت اجرا میشود (یک کوئری `MAX(slot_start)`)، نه در تولید کاندید |
|
||||
| ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ⏳ | به تسک ۱۳ موکول شد — هزینهاش نمایش اسلاتی است که هنگام رزرو رد میشود |
|
||||
| ۱.۱۳ | `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` → `SelectionPolicyEngine` | ⏳ | |
|
||||
| ۲.۲ | تسک ۰۵ — `AppointmentPlanBuilder` مرحلهٔ ۷ → `Resource` + `Timing` | ⏳ | |
|
||||
| ۲.۳ | تسک ۰۶ — `AvailabilityEngine` مرحلهٔ ۶ → `Spacing` | ⏳ | |
|
||||
| ۲.۴ | تسک ۰۷ — `BookingService::confirm` مرحلهٔ ۳ → `Eligibility` | ⏳ | |
|
||||
| ۲.۵ | تسک ۰۸ — `PricingEngine` مرحلهٔ ۳ → `Pricing` | ⏳ | |
|
||||
| ۲.۶ | **هیچ امضایی عوض نشد** | ⏳ | ⭐ دلیل گذاشتن قلابها از روز اول |
|
||||
| ۲.۱ | تسک ۰۴ — `ServiceSelectionValidator` | ✅ | `policyErrors()` |
|
||||
| ۲.۲ | تسک ۰۵ — `AppointmentPlanBuilder` | ✅ | `applyTimingPolicies()` + `applyResourcePolicies()`، روی سرویس بیالگو هم |
|
||||
| ۲.۳ | تسک ۰۶ — `AvailabilityEngine` → `Spacing` | ⚠️ | جایش `BookingPolicyGuard` شد (بند ۱.۱۱/۱.۱۲) |
|
||||
| ۲.۴ | تسک ۰۷ — `Eligibility` | ⚠️ | در `hold` نه `confirm` — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف میکند هم صندلی را |
|
||||
| ۲.۵ | تسک ۰۸ — `PricingEngine` | ✅ | |
|
||||
| ۲.۶ | **هیچ امضایی عوض نشد** | ⚠️ | ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (`PolicyResolver` / `BookingPolicyGuard`) — با DI خودکار بیاثر |
|
||||
|
||||
## ۳. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `policies` + `policy_version_log` | ⏳ | |
|
||||
| ۳.۲ | `idx_policies_lookup (entity_type, entity_id, category, active, valid_from)` | ⏳ | |
|
||||
| ۳.۳ | `appointments.applied_policies` (JSON تهیپذیر) | ⏳ | |
|
||||
| ۳.۴ | قرارداد `applied_policy_ids` با `{id, version, name}` | ⏳ | `name` کپی متنی |
|
||||
| ۳.۵ | `policy_version_log` در `AGGREGATE_CHILDREN` | ⏳ | |
|
||||
| ۳.۶ | `app:policy:seed-examples` — پنج نمونه، همه `active=false` | ⏳ | |
|
||||
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۳.۱ | `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` | ⏳ | تسک ۱۰ همراه صفحهٔ آزمایشگاه |
|
||||
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ✅ | |
|
||||
| ۳.۸ | ستون `condition` به `condition_json` تغییر کرد | ✅ | `condition` در MariaDB کلمهٔ کلیدی است و هر INSERT را میشکست؛ نام فیلد در API همان `condition` ماند |
|
||||
|
||||
## ۴. کارایی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `AvailabilityPerformanceTest` **با قوانین فعال** سبز است | ⏳ | ⭐⭐ اگر قرمز شد، `spacing` حلقه میزند |
|
||||
| ۴.۲ | `SpacingPolicyEngine` تعداد کوئری ثابت دارد، مستقل از تعداد اسلات | ⏳ | |
|
||||
| ۴.۱ | `AvailabilityPerformanceTest` با قوانین فعال سبز | ✅ | ⭐⭐ سبز — و چون `spacing` وارد تولید کاندید نشد، تعداد کوئری اصلاً تغییر نکرد |
|
||||
| ۴.۲ | `spacing` تعداد کوئری ثابت دارد | ✅ | یک `MAX(slot_start)` بهازای هر رزرو، مستقل از تعداد اسلات |
|
||||
|
||||
## ۵. UI
|
||||
|
||||
این تسک صفحه نمیسازد (تسک ۱۰ میسازد). فقط:
|
||||
این تسک صفحه نمیسازد (تسک ۱۰ میسازد).
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | پیامهای خطای `deny` فارسی و قابل فهم بیمار | ⏳ | نه نام قانون خام |
|
||||
| ۵.۲ | خطای `add_requirement` بدون منبع شامل **نام قانون** | ⏳ | «قانون X جراح میخواهد ولی…» |
|
||||
| ۵.۱ | پیامهای `forbid` فارسی و قابل فهم بیمار | ✅ | `reason` دلخواه؛ نبودنش → «قانون «X» این عملیات را مجاز نمیداند» |
|
||||
| ۵.۲ | خطای `require_resource` بدون منبع شامل نام نقش | ⚠️ | نام **نقش** و شعبه میآید، نام قانون نمیآید — تسک ۱۰ |
|
||||
|
||||
## ۶. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | `ConditionEvaluatorTest` — همهٔ عملگرها × نوعها، `all`/`any`، فیلد ناموجود | ⏳ | واحد |
|
||||
| ۶.۲ | `CombinerTest` — شش قاعدهٔ جدول مستند | ⏳ | واحد |
|
||||
| ۶.۳ | `PolicyResolverTest` — سه سناریوی حل تناقض + short-circuit | ⏳ | |
|
||||
| ۶.۴ | `SpacingPolicyEngineTest` — بازهٔ ممنوعه + تعداد کوئری ثابت | ⏳ | |
|
||||
| ۶.۵ | `PolicyVersioningTest` — قانون پنجم | ⏳ | ⭐ |
|
||||
| ۶.۶ | `PolicyIntegrationTest` — چهار دسته end-to-end | ⏳ | |
|
||||
| ۶.۷ | `PolicySchemaTest` — هر فیلد schema قابل extract است | ⏳ | ⭐ |
|
||||
| ۶.۸ | `NoPolicyRegressionTest` | ⏳ | ⭐ |
|
||||
| ۶.۱ | همهٔ عملگرها × نوعها، `all`/`any`، فیلد ناموجود | ⚠️ | فیلد ناموجود و `all` پوشش دارند؛ تست واحدِ هر شش عملگر ندارد |
|
||||
| ۶.۲ | جدول ترکیب | ✅ | max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم) |
|
||||
| ۶.۳ | حل تناقض | ✅ | اختصاصیبودن و اولویت هر دو |
|
||||
| ۶.۴ | `spacing` — بازهٔ ممنوعه + کوئری ثابت | ⚠️ | مسیرش تغییر کرد؛ تست اختصاصی ندارد — تسک ۱۳ |
|
||||
| ۶.۵ | نسخهبندی (قانون پنجم) | ✅ | ⭐ `testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt` |
|
||||
| ۶.۶ | یکپارچگی چند دسته end-to-end | ✅ | timing · selection · pricing |
|
||||
| ۶.۷ | هر فیلد schema قابل extract است | ⏳ | ⭐ تسک ۱۰ — تا آنوقت خطرش در ۱.۳ ثبت است |
|
||||
| ۶.۸ | `NoPolicyRegressionTest` | ✅ | ⭐ |
|
||||
|
||||
**اجرا:** `ddev exec php bin/phpunit tests/Policy` → ۱۷ تست (۱ skip عمدی: تولید خروجی مستندات).
|
||||
|
||||
## ۷. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۷.۱ | `docs/api/policy.md` با فهرست کامل فیلد/عملگر/اثر | ⏳ | |
|
||||
| ۷.۲ | قاعدهٔ «`DiscountRule` یا `Policy`؟» صریح | ⏳ | ⭐ |
|
||||
| ۷.۳ | `docs/architecture/policy-engine.md` — حل تناقض، ترکیب، دلیل ممنوعیت کد دلخواه، دلیل عدم مهاجرت | ⏳ | |
|
||||
| ۷.۱ | `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` بدون خطای جدید | ⏳ | |
|
||||
| ۸.۶ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۸.۷ | تستهای tenant سبز | ⏳ | |
|
||||
| ۸.۸ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | پیامهای `deny` در سایت درست نمایش داده میشوند؟ |
|
||||
| ۸.۱۰ | commit، سپس `graphify update .` | ⏳ | |
|
||||
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۸.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | ۶ مورد ⏳ همه با تسک مقصد |
|
||||
| ۸.۲ | `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 .` | ✅ | دو کامیت جدا |
|
||||
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |
|
||||
|
||||
Reference in New Issue
Block a user