«بیمار بتواند وقت را سریع و امن جابهجا کند» تا لحظهٔ race، DST یا retry بعد از timeout خوشآهنگ است. سپس تیم نمیداند 200 یعنی چه، کدام رزرو باید بیتغییر بماند، یا refund مجاز است. Behavioral Spec قرارداد قابلمشاهده است: outcome، خطا، عدماثر و معیار سنجش را پیش از design تثبیت میکند تا Agent و انسان یک رفتار را بسازند و بررسی کنند.
## اهداف یادگیری
پس از این درس میتوانی Spec رفتاری کامل reschedule را با Scope، role، پیششرط، requirement، acceptance example، API outcome، compatibility، NFR، concurrency، idempotency و privacy بنویسی؛ policy را از mechanism جدا کنی؛ و Ruleها را به test قابلبررسی trace کنی.
## جای درس در چرخهٔ سرتاسری
درس ۱۲ تعیین کرد که این Spec در تغییر حاضر spec-first و spec-anchored است: پیش از code نوشته میشود و مرجع review باقی میماند. فصل ۲ مثالها و Decision ledger را آماده کرده و فصل ۳ pathهای مخزن را کشف کرده است. اکنون Specify تکمیل میشود؛ درس ۱۴ تنها mechanismهای برآوردهکردن این قرارداد را انتخاب میکند و درس ۱۵ اجرای آن را برنامهریزی میکند.
## Artifact کامل: Reschedule appointment v1.0
متن زیر artifact canonical این فصل است. همهٔ requirementها `Accepted` هستند؛ پس `DECIDE` پنهانی در مسیر اصلی باقی نمانده است.
# Reschedule appointment v1.0
Status: Accepted — 2026-09-05
## Scope
بیمارِ مالک یک appointment confirmed، آن را در Tenant خودش و برای همان doctor
به یک interval آزاد منتقل میکند. زمان ورودی local است و timezone کلینیک مرجع
تفسیر آن است. endpoint جدید است و appointment موجود را حفظ میکند.
## Non-goals
Refund، لغو، appointment سریالی، تغییر doctor، sync تقویم خارجی، notification
و migration تاریخی در این تغییر وجود ندارند.
## Actors and authority
Patient: فقط مالک appointment خودش را درخواست میکند.
Authenticated API: tenantId و actorId را از credential میگیرد، نه body.
Clinic: timezone مرجع و policy پنجره را نگه میدارد.
Product owner: deadline نهایی را پذیرفته است.
## Preconditions and invariants
Appointment، doctor و clinic در یک Tenant هستند؛ appointment status برابر confirmed
است. interval پایان بعد از آغاز دارد. intervalهای چسبیده مجازند و همپوشانی ندارند.
هر failure، appointment، version و audit business event را بیتغییر میگذارد.
## API
PATCH /v1/appointments/{appointmentId}/reschedule
Header required: Idempotency-Key با 8 تا 128 نویسهٔ قابلچاپ.
Body: { "startLocal": ISO-8601 local date-time, "endLocal": ISO-8601 local date-time }
Success 200: { id, startAt, endAt, version }؛ startAt و endAt UTC هستند.
Errors: 400 INVALID_IDEMPOTENCY_KEY؛ 403 APPOINTMENT_NOT_OWNED؛
404 APPOINTMENT_NOT_FOUND؛ 409 RESCHEDULE_WINDOW_CLOSED؛ 409 SLOT_CONFLICT؛
409 IDEMPOTENCY_KEY_REUSED؛ 409 REQUEST_IN_PROGRESS؛
409 APPOINTMENT_NOT_RESCHEDULABLE؛ 422 INVALID_INTERVAL؛ 422 INVALID_LOCAL_TIME.
هر error body شامل code و correlationId است و PII، SQL یا stack trace ندارد.
## Accepted requirements and examples
R-01 Authorization and isolation: API باید actor/tenant credential را اعتبارسنجی کند.
A-01: مالک Tenant A appointment خودش را میخواهد؛ درخواست ادامه مییابد.
A-02: actor دیگر در Tenant A همان id را میخواهد؛ 403 APPOINTMENT_NOT_OWNED و عدم mutation.
A-03: actor Tenant B همان id را میخواهد؛ 404 APPOINTMENT_NOT_FOUND و عدم mutation.
R-02 Time and eligibility: API فقط interval معتبر local در timezone کلینیک را میپذیرد.
پنجرهٔ reschedule تا پیش از دقیقاً 24 ساعت مانده به startAt باز است؛ در مرز بسته است.
A-04: در America/New_York، 2026-03-08T02:15؛ 422 INVALID_LOCAL_TIME و عدم mutation.
A-05: endLocal برابر یا پیش از startLocal؛ 422 INVALID_INTERVAL و عدم mutation.
A-06: request در startAt منهای دقیقاً 24 ساعت؛ 409 RESCHEDULE_WINDOW_CLOSED و عدم mutation.
A-07: appointment cancelled؛ 409 APPOINTMENT_NOT_RESCHEDULABLE و عدم mutation.
R-03 Conflict: دو request معتبر و همزمان برای interval همپوشان یک doctor در یک Tenant،
دقیقاً یک success 200 میسازند؛ بازنده 409 SLOT_CONFLICT میگیرد.
A-08: appointment A و B interval آزاد 11:00 تا 11:30 میخواهند؛ یک interval confirmed،
یک 200، یک 409 و فقط یک audit RESCHEDULED وجود دارد.
R-04 Success and audit: request مجاز interval همان appointment را تغییر میدهد، version را
یک واحد زیاد میکند و دقیقاً یک audit RESCHEDULED با actorId و correlationId مینویسد.
A-09: appointment A از 09:00-09:30 به 11:00-11:30 میرود؛ id حفظ، UTC response معتبر،
version+1 و یک audit ثبت میشود.
R-05 Idempotency: کلید در محدوده tenantId، actorId و operation نگهداری میشود.
fingerprint شامل appointmentId، startLocal و endLocal است. retry کلید و fingerprint یکسان،
همان status/body نخست را برمیگرداند و mutation یا audit دوم ندارد. fingerprint متفاوت با
کلید یکسان 409 IDEMPOTENCY_KEY_REUSED است. کلید در حال اجرا 409 REQUEST_IN_PROGRESS است.
A-10: timeout پس از commit، retry با همان کلید/body؛ همان 200 و یک audit کل مشاهده میشود.
A-11: همان کلید با endLocal دیگر؛ 409 IDEMPOTENCY_KEY_REUSED و عدم mutation.
R-06 Compatibility and privacy: endpoint موجود PATCH /v1/appointments/{id} و responseهای
آن تغییر نمیکنند. clientهای قدیمی endpoint جدید را فراخوانی نمیکنند؛ client جدید باید
Idempotency-Key بفرستد. response، audit و log فقط شناسههای مجاز و hash کلید را دارند.
A-12: log request شامل key خام، body درمانی یا appointment Tenant دیگر نیست.
## Measurable quality requirements
NFR-01 Latency: artifact اندازهگیری `apps/api/src/modules/appointments/reschedule.latency.bench.ts`
و fixture آن `packages/test-fixtures/src/appointments/reschedule-latency-fixture.ts` است. این
benchmark فقط در sandbox محلی و database disposable با نام
`booking_platform_bench_<run-id>` اجرا میشود؛ staging و production مجاز نیستند. پیش از اجرا
guard زیر باید URL را به localhost و همین نام database محدود کند:
`case "${TEST_DATABASE_URL:-}" in postgresql://*localhost*booking_platform_bench_*|postgres://*localhost*booking_platform_bench_*|postgresql://*127.0.0.1*booking_platform_bench_*|postgres://*127.0.0.1*booking_platform_bench_*) ;; *) echo "use only a local disposable booking_platform_bench database"; exit 1;; esac`.
پس فرمان زیر دقیقاً 100 درخواست موفق A-09 را با concurrency یک اجرا میکند:
`RESCHEDULE_V1_ENABLED=true pnpm exec tsx apps/api/src/modules/appointments/reschedule.latency.bench.ts --database "$TEST_DATABASE_URL" --fixture packages/test-fixtures/src/appointments/reschedule-latency-fixture.ts --samples 100 --concurrency 1`.
fixture کامل `packages/test-fixtures/src/appointments/reschedule-latency-fixture.ts` چنین export میکند:
`{ tenantId:"tenant-latency-a", actorId:"patient-latency-a", appointmentId:"appt-latency-001", doctorId:"doctor-latency-007", patientId:"patient-latency-a", status:"confirmed", existingRevision:41, existingStartAt:"2026-06-20T07:00:00Z", existingEndAt:"2026-06-20T07:30:00Z", clinicTimezone:"America/New_York", requestedStartLocal:"2026-06-20T11:00:00", requestedEndLocal:"2026-06-20T11:30:00", expectedStartAt:"2026-06-20T15:00:00Z", expectedEndAt:"2026-06-20T15:30:00Z", expectedResponseRevision:42, auth:{tenantId:"tenant-latency-a",actorId:"patient-latency-a",role:"patient"}, idempotencyKeyPrefix:"latency-a09-" }`.
Runner پیش از هر sample همان appointment را با `patientId:"patient-latency-a"`، `status:"confirmed"`،
revision 41 و interval قدیمی seed/reset میکند؛ `auth.actorId` و `patientId` هر دو
`"patient-latency-a"` هستند. request نمونهٔ `n` کلید `latency-a09-n`، همین auth مالک و
start/end/timezone بالا را میفرستد. metric،
`p95_server_duration_ms` از ورود route تا ساخت response است؛ threshold حداکثر 400ms است. runner باید
`environment`، `git_revision`، `samples`، `concurrency`، `observed_response_revision`،
`expected_revision_transition=41->42`، metric، threshold و PASS/FAIL را چاپ کند. این **مشاهدهٔ پذیرش
مورد انتظار** است، نه نتیجهٔ اجرای واقعی: `environment=local-disposable git_revision=fc6c40b samples=100 concurrency=1 observed_response_revision=42 expected_revision_transition=41->42 p95_server_duration_ms=372 threshold_ms=400 result=PASS`. این benchmark محلی evidence تشخیصی است، نه اثبات p95 release یا Production.
اگر seed revision برابر 41 نبود یا response revision برابر 42 نشد، runner با exit 1 و
`seed_revision_mismatch` یا `response_revision_mismatch` متوقف میشود؛ P6 seed/reset یا migration را
بررسی میکند و benchmark را با دادهٔ مخلوط تکرار نمیکند.
NFR-02 Reliability: test integration barrierدار در 100 iteration، هر بار دقیقاً یک 200 و
یک 409 میبیند و هرگز دو interval confirmed همپوشان نمییابد.
NFR-03 Privacy: در test منفی A-03 و A-12، response/log هیچ PII، raw key، SQL یا stack trace
ندارند؛ آزمون scan log باید صفر match برای raw fixture patient name داشته باشد.
## Trace
R-01: A-01 تا A-03؛ R-02: A-04 تا A-07؛ R-03: A-08؛ R-04: A-09؛
R-05: A-10 تا A-11؛ R-06: A-12. Technical Design و ExecPlan باید همین شناسهها را حفظ کنند.## چرا این detail لازم است؟
Spec رفتار را، نه SQL یا lock را، تعیین میکند. R-03 فقط outcome race را قفل میکند؛ درس ۱۴ تصمیم میگیرد datastore چگونه invariant را enforce کند. در عوض R-05 باید fingerprint، کلید در حال اجرا و نتیجهٔ replay را تعیین کند، زیرا اینها برای client behavior ضروریاند. تفاوت `404` cross-tenant و `403` owner دیگر نیز policy پذیرفتهشده است، نه انتخاب اتفاقی framework.
NFR عدد جدا از محیط نیست. NFR-01 فقط staging release-matched و requestهای موفق را میسنجد، پس نه production SLA است نه پاسخ conflict. NFR-02 success ظاهری را کافی نمیداند و state داده را هم میسنجد. NFR-03 هم error و log را پوشش میدهد؛ masking UI امنیت tenant نیست.
## مثال واقعی: trace از Spec تا test
برای A-08، fixture دو appointment confirmed برای یک doctor/Tenant میسازد، barrier هر request را پیش از write نگه میدارد و سپس آزاد میکند. assertion فقط «دو response رسید» نیست: یک 200، یک `SLOT_CONFLICT`، یک interval confirmed و یک audit `RESCHEDULED` لازم است. این example R-03 را میسنجد و لایهٔ پیادهسازی را آزاد میگذارد.
برای A-10، test نخست request را commit میکند اما پاسخ را برای client نامعلوم شبیهسازی میکند. request دوم با همان key/body باید response durable را replay کند. اگر audit دوم ظاهر شد، حتی با دو 200 یک defect R-05 رخ داده است.
مثال ضعیف و علت شکست
کاربر باید سریع و امن وقت را جابهجا کند؛ در خطا پیام مناسب نشان بده.این جمله actor، deadline، DST، response، عدم mutation، retry و privacy را حذف میکند. Agent ممکن است status 200 برگرداند، سپس notification/refund ناخواسته بسازد و همچنان ادعا کند مسیر موفق کار میکند.
مثال اصلاحشده
R-02/A-06: request در startAt منهای دقیقاً 24 ساعت، 409 RESCHEDULE_WINDOW_CLOSED
میگیرد؛ appointment، version و audit business event تغییر نمیکنند.
R-05/A-11: کلید یکسان با fingerprint متفاوت، 409 IDEMPOTENCY_KEY_REUSED میگیرد؛
نه mutation و نه audit تازه رخ میدهد.## آزمایشگاه: Spec را trace کن
**ورودی:** artifact کامل بالا و repository map فصل ۳. **گامها:** ۱) برای هر R، test file و assertion پیشنهادی بنویس. ۲) A-08 را با barrier همزمانی و A-10 را با retry بساز. ۳) error body را برای همهٔ codeها بررسی کن. ۴) NFRها را با revision/environment ثبت کن. ۵) non-goalها را با diff scan کنترل کن.
pnpm exec vitest run apps/api/src/modules/appointments/reschedule.int.test.ts**خروجی مورد انتظار:** Vitest برای A-01 تا A-12 سبز است؛ suite A-08 دقیقاً یک success/یک conflict را گزارش میکند و A-10 فقط یک audit دارد. **Recovery:** اگر fixture concurrency را deterministic نمیکند، scheduler/barrier را اصلاح کن؛ sleep یا retry تا سبزشدن evidence نیست. اگر outcome تازه لازم است، Spec v1.0 را بیصدا patch نکن؛ owner باید revision جدید را بپذیرد.
## خطاهای رایج و نشانهها
- **Rule بدون status:** معلوم نیست owner آن را پذیرفته یا Agent حدس زده است.
- **error بدون state:** status مشخص است ولی mutation یا audit failure آزموده نشده است.
- **idempotency کوتاه:** key هست اما fingerprint، in-progress یا replay ندارد.
- **NFR نمایشی:** عدد بدون command، revision یا جمعیت است.
- **Scope creep:** refund یا notification در task ظاهر میشود ولی non-goal تغییر نکرده است.
## چکلیست Verification
- [ ] R-01 تا R-06 و A-01 تا A-12 status و outcome کامل دارند.
- [ ] actor، tenant، timezone، deadline و invalid-state صریحاند.
- [ ] API code/body و compatibility policy تعیین شدهاند.
- [ ] failureها عدم mutation/version/audit را تعیین میکنند.
- [ ] NFRها محیط، sample، threshold و Oracle دارند.
- [ ] trace Spec به design، plan و test حفظ شده است.
## تمرین مستقل
برای «لغو appointment با refund احتمالی»، Spec کامل اما بدون اختراع policy مالی بنویس.
**Rubric پاسخ:** actor/tenant، transition و errorها کاملاند؛ refund `DECIDE` با owner باقی میماند؛ retry key/fingerprint و audit را تعریف میکند؛ NFR privacy/latency measurement دارد؛ و هر example outcome و عدماثر failure را میگوید.
## جمعبندی
Behavioral Spec کامل یعنی Rule، مثال، response، state و evidence هممعنا هستند. این artifact deadline، invalid state، concurrency، replay، compatibility و privacy را پیش از design قطعی میکند. درس بعد از R-01 تا R-06 mechanism میسازد، نه requirement تازه.
## منابع مستقیم
- [EARS requirements syntax](https://doi.org/10.1109/RE.2009.9) — نحو ساختیافته برای روشنکردن نیاز، نه تضمین کاملبودن Spec.
- [Gojko Adzic — Specification by Example](https://gojko.net/books/specification-by-example/) — روش جامعهمحور برای مثالهای قابل گفتگو و آزمون.
- [Dan North — Introducing BDD](https://dannorth.net/blog/introducing-bdd/) — منبع Given/When/Then؛ جایگزین policy و design نیست.
- [ISO/IEC/IEEE 29148:2018](https://www.iso.org/standard/72089.html) — استاندارد requirements engineering و traceability.
به زبان ساده
Behavioral Spec نتیجه، خطا، عدم mutation، سازگاری و معیار اندازهگیری را پیش از انتخاب مکانیزم تثبیت میکند. Requirementی که فقط «سریع و امن» میگوید، برای test و review Oracle کافی ندارد.
مثال واقعی
در artifact فرضی دوره، A-08 برای دو درخواست همزمان reschedule دقیقاً یک `200` و یک `409 SLOT_CONFLICT` میخواهد و یک audit موفق را حفظ میکند؛ این رفتار implementation را به قفل خاصی مقید نمیکند.