সরাসরি পর্বে যান
পর্ব ৬৫ · ৭০-এর মধ্যে · মডিউল ১০

ক্যাপস্টোন — requirement থেকে release পর্যন্ত একটি পূর্ণ project, Git workflow-সহ

Capstone — One Complete Project from Requirements to Release, with a Git Workflow

Taught in Bangla, this lesson runs one small project from inception to retrospective: user stories and measurable NFRs, estimation and planning, design and ADRs, repository setup with branch protection, TDD with Python unittest, two developers' pull requests, conflicts and rebase, a CI pipeline, tags, CHANGELOG and SemVer releases, and a post-release bug found with git bisect run, all with real Git transcripts.

এই পর্বে যা শিখবেন

  • একটি ছোট project-কে requirement, estimation, plan আর design-এর artefact-এ রূপ দেওয়া — আর প্রতিটি artefact কোন আগের পর্বের দক্ষতা প্রয়োগ করে
  • Repository শুরু থেকে সাজানো: .gitignore, README, CONTRIBUTING, branch protection-এর নীতি আর Conventional Commits
  • TDD-তে Python unittest দিয়ে core module, আর দুজনের feature branch, PR review, conflict, git rebase main আর --no-ff merge
  • CI pipeline, annotated tag, CHANGELOG আর SemVer দিয়ে release; release-এর পরে bug report থেকে git bisect run, regression test আর patch release
  • Commit-এর আসল সময় থেকে retrospective-এর মেট্রিক বের করা, ঝুঁকি চেনা, আর নিজের project-এর জন্য একটি পুনরাবৃত্তিযোগ্য চেকলিস্ট

১ · ক্যাপস্টোনের মানচিত্র — একটি ছোট project, দশটি ধাপ

আগের ৬৪টি পর্বে আপনি আলাদা আলাদা দক্ষতা শিখেছেন — requirement লেখা, নকশা, test, Git, CI, estimation. বাস্তবে এগুলো আলাদা আসে না; একটি project-এ সবগুলো একসাথে, একে অন্যের ওপর নির্ভর করে। এই ক্যাপস্টোন পর্বে একটি ছোট কিন্তু সম্পূর্ণ project শুরু থেকে শেষ পর্যন্ত চলবে। নতুন তত্ত্ব প্রায় নেই; প্রতিটি ধারণা এক বাক্যে আর বিস্তারিত পর্বের লিংকে। পথটি অনুসরণ করুন, command নিজে চালান, শেষে নিজের ধারণায় আবার হাঁটুন।

Project: একটি কাল্পনিক পাড়ার পাঠাগারের বই ধারের হিসাব রাখার ছোট Python module, নাম lending. সদস্য বই ধার নেন, ১৪ দিনের মধ্যে ফেরত দেন, দেরি হলে দিনে ৫ টাকা জরিমানা; লাইব্রেরিয়ান দেখতে চান কোন বইগুলোর মেয়াদ পেরিয়েছে। দলে দুজন developer: Rahim (repository-র maintainer) আর Nasrin; product owner-এর ভূমিকায় পাঠাগারের লাইব্রেরিয়ান। নাম, পাঠাগার আর সংখ্যা সবই কাল্পনিক।

একটি বিয়েবাড়ির আয়োজন ভাবুন। প্রথমে অতিথির তালিকা আর কী কী খাওয়ানো হবে (requirement), তারপর বাজেট আর কোন কাজ কার আগে (estimation আর plan), তারপর মেনু আর রান্নার নকশা (design), একটি হিসাবের খাতা যেখানে প্রতিটি বাজার লেখা (repository), প্রতিটি পদ চেখে দেখা (test), দুই বাবুর্চির মধ্যে কে কোন চুলা নেবেন (দলগত কাজ), পরিবেশনের আগে কেউ একবার সব দেখে নেন (CI), পরিবেশন (release), এক অতিথির অভিযোগ “তরকারিতে লবণ কম” (bug report), আর পরের দিন সবাই বসে কী ভালো গেল, কী খারাপ (retrospective). ধাপ বাদ দিলে দাম পরে দিতে হয়।

ধাপযা তৈরি হয় (artefact)যে দক্ষতা প্রয়োগ হয়
১ · Inception আর requirementstakeholder তালিকা, ৬টি user story, Given-When-Then, ২টি NFRপর্ব ২০–২৪
২ · Estimation আর planstory point, ৭টি task-এর CPM তালিকা, দুটি sprintপর্ব ৫, ৬০, ৬১
৩ · Designdomain model, class diagram, ADR 0001পর্ব ২৫–৩১, ৩৮
৪ · Repositoryinit, .gitignore, README, CONTRIBUTING, branch protection-এর নীতিপর্ব ৭–১০, ১৬–১৮
৫ · TDD দিয়ে buildlending.py, test_*.pyপর্ব ৪৫–৫১
৬ · দলগত কাজfeature branch, PR review, conflict-এর সমাধান, rebase, --no-ff mergeপর্ব ১২–১৭, ৪৪
৭ · CI.github/workflows/ci.ymlপর্ব ৫৮
৮ · ReleaseCHANGELOG, annotated tag v1.0.0পর্ব ১৮
৯ · Release-এর পরেbug report, bisect, regression test, v1.0.1পর্ব ১১, ৫৫
১০ · Retrospectiveমেট্রিক, burndown, retro-র সিদ্ধান্ত, risk registerপর্ব ৫, ৫৯, ৬২

নিচের stepper-এ পুরো সময়রেখা এক নজরে। এই দলের কর্মসপ্তাহ রবিবার থেকে বৃহস্পতিবার; Sprint ১ হলো ১–৫ ফেব্রুয়ারি ২০২৬, Sprint ২ হলো ৮–১২ ফেব্রুয়ারি। প্রতিটি ধাপে দেখুন কোন artefact তৈরি হলো আর main-এর মাথা কোন commit-এ।

lending · কাল্পনিক পাঠাগারের বই ধার service
তারিখ
ধাপ
Artefact
main-এর মাথা
কে
  1. Inception. লাইব্রেরিয়ানের সাথে এক ঘণ্টার বৈঠক: stakeholder, সমস্যা আর ছয়টি user story-র খসড়া (অংশ ২)।
  2. Plan. Planning poker-এ story point, তারপর সাতটি task-এর নির্ভরতা — critical path সাত কর্মদিবস (অংশ ৩)।
  3. Design. Domain model, class diagram আর একটি লেখা সিদ্ধান্ত — “v1-এ ধার memory-তে” (ADR 0001).
  4. Repository. Rahim repository বানিয়ে প্রথম দুটি commit দেন — কাঠামো আর CI — তারপর branch protection চালু করেন। Nasrin clone করেন (অংশ ৪)।
  5. TDD. প্রতিটি story-র জন্য আগে ব্যর্থ test, তারপর ন্যূনতম কোড, তারপর সবুজ test-সহ একটি commit (অংশ ৫)।
  6. দলগত কাজ. একই base থেকে দুটি branch; PR #1 আগে merge, তাই PR #2-এ conflict — Nasrin rebase করে মেটান (অংশ ৬)।
  7. CI. প্রথম দিন থেকেই প্রতিটি push আর PR-এ lint আর test চলে; লাল check থাকলে merge বন্ধ (অংশ ৭)।
  8. Release. CHANGELOG লিখে একটি release PR, তারপর annotated tag v1.0.0 (অংশ ৭)।
  9. Bug. আগে ফেরত দেওয়া বইয়ে জরিমানা −২০ টাকা; bisect, regression test-সহ fix, দুপুরের আগে v1.0.1 (অংশ ৮)।
  10. Retrospective. Git-এর সময় থেকে মেট্রিক, burndown, কী ভালো গেল আর কী বদলাবেন, আর ঝুঁকির তালিকা (অংশ ৯)।

নিজে চালিয়ে দেখবেন কীভাবে। পর্বের প্রতিটি Git transcript Git ২.৫৩-এ (Windows, Git Bash) একটি ফাঁকা ফোল্ডারে চালানো: একটি bare repository origin.git “server”-এর কাজ করে, আর তার দুটি clone — rahim/ আর nasrin/. প্রতিটি commit-এর আগে GIT_AUTHOR_DATE আর GIT_COMMITTER_DATE একটি নির্দিষ্ট সময়ে বসানো (যেমন 2026-02-02T11:00:00+06:00), নাম Rahim বা Nasrin, email @example.com, আর core.autocrlf false. File-এর একটি byte-ও আলাদা হলে hash আলাদা হবে — পর্ব ৫২-এ (Git-এর ভেতরে) দেখেছেন কেন। আসল platform-এ PR আর merge web page-এ হয়; এখানে merge স্থানীয় Git command-এ করে push, আর PR-এর সময় ও মন্তব্য কাল্পনিক PR page থেকে দেওয়া তথ্য.

২ · ধাপ ১ — Inception: stakeholder, user story আর মাপা যায় এমন NFR

প্রথম কাজ কোড নয়, প্রশ্ন: Inception-এর বৈঠকে কার সমস্যা, কেন, আর কতটুকু দিয়ে শুরু। পর্ব ২০-এ (Requirement engineering) শিখেছেন, প্রতিটি stakeholder-এর স্বার্থ আলাদা, আর তাঁদের খুঁজে না পেলে requirement-এ ফাঁক থাকে।

Stakeholderতাঁর চাওয়াপ্রভাব
লাইব্রেরিয়ান (product owner)খাতার বদলে নির্ভুল হিসাব; কোন বই দেরিতে, এক নজরেউচ্চ — অগ্রাধিকার ঠিক করেন
সদস্য (পাঠক)সহজে ধার, ন্যায্য জরিমানামাঝারি — অভিযোগই প্রথম সংকেত
পাঠাগার কমিটিধারের মেয়াদ (১৪ দিন) আর জরিমানার হার (দিনে ৫ টাকা) তাঁদের সিদ্ধান্তউচ্চ — নিয়ম বদলাতে পারেন
হিসাবরক্ষকজরিমানার টাকা কখনো ঋণাত্মক বা ভুল নয়মাঝারি
Developer (Rahim, Nasrin)সহজে বদলানো যায় এমন কোড, দ্রুত feedbackনকশা আর process ঠিক করেন

এক লাইনের লক্ষ্য (vision): “পাঠাগারের ধার আর জরিমানার হিসাব নির্ভুলভাবে রাখা, যাতে লাইব্রেরিয়ান দেরির বই এক নজরে দেখতে পান।” এরপর ছয়টি user story, প্রতিটির acceptance criteria Given-When-Then আকারে (পর্ব ২২, Use case আর user story). শব্দগুলো আগে থেকে একটি ছোট গ্লোসারিতে ঠিক করা: “ধার” = loan, “ফেরত” = give back, “দেরি” = মেয়াদের (১৪ দিন) পরের দিনগুলো।

IDUser storyAcceptance criteria (Given-When-Then)
S1সদস্য হিসেবে আমি একটি বই ধার নিতে চাই, যাতে বাড়িতে পড়তে পারি।Given B1 বইটি কারও কাছে নেই, When M1 ২ ফেব্রুয়ারি B1 ধার নেন, Then ধারের তালিকায় B1 → (M1, ২ ফেব্রুয়ারি)।
S2সদস্য হিসেবে আমি বই ফেরত দিতে চাই, যাতে অন্যরা পান।Given B1 M1-এর কাছে, When তিনি ফেরত দেন, Then B1 আর ধারের তালিকায় নেই।
S3লাইব্রেরিয়ান হিসেবে আমি চাই একই বই একসাথে দুজনকে ধার দেওয়া না হোক।Given B1 M1-এর কাছে, When M2 B1 চান, Then অনুরোধ বাতিল হয় (ValueError) আর তালিকা বদলায় না।
S4কমিটি হিসেবে আমি চাই দেরিতে ফেরতে জরিমানা হোক, যাতে বই সময়মতো ফেরে।AC1: Given ২ ফেব্রুয়ারি ধার, When ঠিক ১৪ দিনে ফেরত, Then জরিমানা ০। AC2: When ১৭ দিনে ফেরত, Then ১৫ টাকা। AC3: ফেরতের সময়েই জরিমানা জানা যায়।
S5লাইব্রেরিয়ান হিসেবে আমি মেয়াদ-পেরোনো বইয়ের তালিকা চাই।Given B1, B2 ২ ফেব্রুয়ারি আর B3 ১২ ফেব্রুয়ারি ধার, When ১৭ ফেব্রুয়ারি তালিকা চাওয়া হয়, Then [B1, B2] — id অনুযায়ী সাজানো, B3 নয়।
S6কমিটি হিসেবে আমি চাই একজন সদস্য একসাথে ৩টির বেশি বই না নিন।(পরের release-এর জন্য backlog-এ; অনুশীলন ১-এ আপনি নিজে করবেন)

লক্ষ করুন, একটি কথা কোথাও লেখা নেই: “মেয়াদের আগে ফেরত দিলে জরিমানা ০।” সবাই ধরে নিয়েছেন এটি স্বাভাবিক — তাই কেউ লেখেননি, আর পরে কেউ test-ও লেখেননি। এই নীরব অনুমানই অংশ ৮-এর bug. পর্ব ২১-এ (Functional আর non-functional requirement) দেখেছেন, “স্পষ্ট” কথাগুলোই সবচেয়ে বেশি বাদ পড়ে।

Story-গুলো INVEST-এর মাপে মোটামুটি ঠিক। দুটি non-functional requirement, সংখ্যাসহ — কারণ “দ্রুত হবে” বা “ভালো মানের হবে” test করা যায় না:

  • NFR-1 (performance efficiency): ১০,০০০টি চলমান ধারের মধ্যে overdue() একটি সাধারণ laptop-এ ২০০ মিলিসেকেন্ডের কম সময়ে উত্তর দেয় (৫ বার চালানোর median).
  • NFR-2 (maintainability): main-এর প্রতিটি commit-এ সব unit test সবুজ; CI-এর ফল ৫ মিনিটের মধ্যে আসে; প্রতিটি acceptance criterion-এর অন্তত একটি test আছে।

দুটি NFR-ই ISO/IEC 25010-এর quality characteristic-এর নামে (পর্ব ৩, Software quality). আর শেষ শর্তটি traceability: প্রতিটি AC থেকে একটি test-এর নাম পর্যন্ত সুতো টানা যায় (পর্ব ২৪, Requirement management). যেমন S3 → test_book_on_loan_cannot_be_borrowed_again. এই সুতো পরে দেখাবে কোথায় ছিঁড়েছিল।

৩ · ধাপ ২ ও ৩ — Estimation, plan আর design

Story point আর planning poker

পর্ব ৬০-এ (Software estimation) দেখেছেন, story point সময় নয়, আপেক্ষিক আকার। দুজনে planning poker খেললেন। S4-এ (জরিমানা) প্রথম দফায় Rahim দিলেন ৩, Nasrin ৮। কারণ জিজ্ঞেস করতেই বেরোল: Nasrin ধরে নিয়েছিলেন জরিমানা ফেরতের সময় give_back-এর সাথে যুক্ত করতে হবে আর তার test-ও লাগবে (AC3), Rahim শুধু হিসাবের function ভেবেছিলেন। আলোচনার পরে দুজনেই ৫। পোকারের আসল লাভ সংখ্যা নয়, এই লুকানো অনুমান বের হয়ে আসা।

StoryPointSprintমালিক
S1 ধার৩১Rahim
S2 ফেরত২১Rahim
S3 দুবার ধার নয়১১Rahim
S4 জরিমানা৫১Nasrin
S5 মেয়াদ-পেরোনো তালিকা৩২Rahim
S6 সর্বোচ্চ ৩টি বই৩২ (পরে বাদ)—

Task list আর critical path

Story point দিয়ে sprint ভরানো যায়, কিন্তু “কোন কাজ কার আগে” বোঝা যায় না। তাই কাজগুলো সাতটি task-এ ভাঙা হলো, প্রতিটির সময় কর্মদিবসে আর নির্ভরতা — একটি ছোট CPM-এর তালিকা (পর্ব ৬১, Project planning). ES/EF = সবচেয়ে আগে শুরু/শেষ (forward pass), LS/LF = সবচেয়ে দেরিতে শুরু/শেষ (backward pass), float = LS − ES:

Taskদিনআগে লাগেES–EFLS–LFFloat
T1 repository আর CI১—০–১০–১০
T2 ধার, ফেরত (S1–S3)৩T1১–৪১–৪০
T3 জরিমানার হিসাব (S4)২T1১–৩২–৪১
T4 ফেরতের সাথে জরিমানা যুক্ত১T2, T3৪–৫৪–৫০
T5 মেয়াদ-পেরোনো তালিকা (S5)১T2৪–৫৫–৬১
T6 পরিচ্ছন্নতা আর README১T4৫–৬৫–৬০
T7 release১T5, T6৬–৭৬–৭০

Critical path T1 → T2 → T4 → T6 → T7 = ১ + ৩ + ১ + ১ + ১ = ৭ কর্মদিবস, যার কোনো task পিছোলে release পিছোবে। T3 আর T5-এর হাতে এক দিন করে float. পরিকল্পনা বলছিল সাত দিন; বাস্তবে release হলো দশম কর্মদিবসে (১২ ফেব্রুয়ারি)। কেন — সেটি retrospective-এর সংখ্যায় পাবেন: review-এর অপেক্ষা কোনো task-এ ধরা ছিল না।

Domain model, class diagram আর নকশার নীতি

পর্ব ২৩-এর (Requirement modeling) মতো story-র বিশেষ্যগুলো থেকে domain model: Member, Book, Loan (কোন বই, কোন সদস্য, কবে), আর সবকিছু ধরে রাখা Library. নিয়মগুলো — ১৪ দিন, দিনে ৫ টাকা — আলাদা, কারণ কমিটি সেগুলো বদলাতে পারেন। নিচে UML class diagram (পর্ব ২৯, UML class diagram):

lending-এর class diagram — Library, Loan, Book, Member আর নিয়মের module

filled diamond = composition · open arrow = navigable association · dashed arrow = dependency

lending-এর UML class diagram Library «utility» rules Loan Book Member - loans: {book: Loan} + borrow(book, member, day) + give_back(book, day): int + overdue(today): list LOAN_DAYS = 14 FINE_PER_DAY = 5 days_late(b, t) fine(b, r): int borrowed_on: date /due_on = borrowed_on + 14 id: str id: str «use» 10..* *1 *1 কোডে Loan একটি tuple (member, borrowed_on); Book আর Member শুধু id — ADR 0001 দেখুন
ছবি ৬৫.১ — Loan-এর জীবন Library-র ভেতরে (composition: Library না থাকলে loan-এর হিসাবও নেই), প্রতিটি Loan ঠিক একটি Book আর একটি Member-কে চেনে। নিয়মগুলো আলাদা module-এ, Library শুধু সেগুলো ব্যবহার করে (dependency). /due_on-এর সামনের / মানে derived attribute — হিসাব করে পাওয়া, আলাদা জমা নয়।

কয়েকটি নীতি এক বাক্যে: জরিমানার হিসাব state-হীন pure function — single responsibility আর সহজ test (পর্ব ২৭, SOLID ১); Library নিয়মের মান জানে না — কম coupling, বেশি cohesion (পর্ব ২৫, Coupling আর cohesion); give_back-এর precondition “বইটি ধারে আছে”, নইলে KeyError (পর্ব ৩১, Interface আর contract); আর database বা user interface এখন নয় (YAGNI, পর্ব ২৬)।

যে সিদ্ধান্তের পেছনে বিকল্প ছিল, সেটি লিখে রাখা হলো একটি ADR-এ (পর্ব ৩৮, Software architecture) — repository-র প্রথম commit-এই:

# ADR 0001: Keep loans in memory for v1

Status: accepted (2026-02-01)

Context: v1 is a teaching demo; no persistence requirement yet.
Decision: store loans in a dict inside Library.
Consequences: data is lost on restart; a storage port can be
added later without changing the borrow and return rules.

প্রসঙ্গ, সিদ্ধান্ত, ফল — ছয় মাস পরে “database নেই কেন?” প্রশ্নের উত্তর repository-তেই।

৪ · ধাপ ৪ — Repository: init, .gitignore, README, CONTRIBUTING আর branch protection

প্রথম দিনের লক্ষ্য একটি walking skeleton: প্রায় ফাঁকা, কিন্তু পুরো পথ চলে — repository, একটি module, test চালানোর নির্দেশ আর CI. Rahim নিজের যন্ত্রে শুরু করলেন (পর্ব ৮, প্রথম Git repository; পর্ব ১০, .gitignore আর git config):

$ git -c init.defaultBranch=main init
Initialized empty Git repository in …/rahim/.git/
$ git config user.name "Rahim"
$ git config user.email "rahim@example.com"
$ git config core.autocrlf false
$ git config pull.ff only
$ git config fetch.prune true
$ git add .
$ git status --short
A  .gitignore
A  CONTRIBUTING.md
A  README.md
A  docs/adr/0001-in-memory-store.md
A  lending.py
$ git commit -m "chore: initial project skeleton"
[main (root-commit) a323299] chore: initial project skeleton
 5 files changed, 30 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 CONTRIBUTING.md
 create mode 100644 README.md
 create mode 100644 docs/adr/0001-in-memory-store.md
 create mode 100644 lending.py
$ git add .github
$ git commit -m "ci: add lint and test workflow"
[main 4a99fdc] ci: add lint and test workflow
 1 file changed, 23 insertions(+)
 create mode 100644 .github/workflows/ci.yml
$ git remote add origin ../origin.git
$ git push -u origin main
To ../origin.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

init.defaultBranch=main নতুন repository-র প্রথম branch-এর নাম ঠিক করে (না দিলে Git-এর পুরোনো ডিফল্ট master, সাথে একটি hint). pull.ff only মানে git pull কখনো নিজে থেকে merge commit বানাবে না — local main শুধু সামনে এগোবে; fetch.prune true server-এ মুছে ফেলা branch-এর পুরোনো remote-tracking branch নিজে থেকে সরায়। প্রথম commit-এ পাঁচটি file, যার দুটি দলের নিয়ম:

# .gitignore
__pycache__/
*.pyc
.venv/
.env

# CONTRIBUTING.md
# Contributing

1. One branch per story: feature/<name>, fix/<name>.
2. Write the failing test first; every commit on main stays green.
3. Commit messages follow Conventional Commits 1.0.0.
4. Open a pull request; one approval and a green CI are required.
5. Rebase on main before merge; merge with --no-ff.

.gitignore-এ Python-এর cache, virtual environment আর .env — শেষেরটি থাকলে ভুল করে password commit হওয়ার পথ বন্ধ (পর্ব ৫৭, Git-এ নিরাপত্তা)। README বলে project কী আর test কীভাবে চালাতে হয়; CONTRIBUTING বলে কাজ কীভাবে ঢোকে। দলের নিয়ম লেখা থাকলে review-এ তর্ক কমে — “নিয়ম ৫ দেখুন” বলাই যথেষ্ট।

Branch protection-এর নীতি

দুটি commit push হওয়ার পর Rahim platform-এর settings-এ main-এর branch protection চালু করলেন। নামগুলো platform-ভেদে আলাদা, আর লেখার সময়ের GitHub, GitLab বা Bitbucket-এ একই রকম বিকল্প আছে (পর্ব ১৬, Pull request আর fork):

  • main-এ সরাসরি push নিষেধ; সব পরিবর্তন pull request-এ।
  • অন্তত একজনের approval লাগবে, আর লেখক নিজের PR approve করতে পারবেন না।
  • Required status check: CI-এর test job সবুজ না হলে merge বোতাম বন্ধ।
  • main-এ force push আর branch মুছে ফেলা নিষেধ।
  • Merge হলে PR-এর branch মুছে ফেলা হয়।

প্রথম দুটি commit এই নিয়ম চালুর আগে সরাসরি গেছে — নতুন repository-তে এটিই স্বাভাবিক “bootstrap” ব্যতিক্রম; এর পরে কোনো ব্যতিক্রম নেই, maintainer-এর জন্যও না। Branching strategy হিসেবে দলটি ছোট আয়ুর feature branch আর একটি main নিল — GitHub Flow-এর কাছাকাছি (পর্ব ১৭, Branching strategy).

এবার Nasrin clone করলেন (পর্ব ১৪, Remote repository):

$ git clone -c core.autocrlf=false origin.git nasrin
Cloning into 'nasrin'...
done.
$ git remote set-url origin ../origin.git
$ git config user.name "Nasrin"
$ git config user.email "nasrin@example.com"
$ git config pull.ff only
$ git config fetch.prune true
$ git log --oneline
4a99fdc ci: add lint and test workflow
a323299 chore: initial project skeleton

-c core.autocrlf=false clone-এর মুহূর্তেই দেওয়া, কারণ এই Windows যন্ত্রের global setting-এ autocrlf চালু; পরে বদলালে file-গুলো CRLF-এ থেকে যেত আর git status সব file “modified” দেখাত — প্রথম চেষ্টায় তা-ই ঘটেছিল। Local path থেকে clone-এ Git URL-টি পূর্ণ path হিসেবে রাখে; set-url আপেক্ষিক path দেয়, যাতে output-এ এই যন্ত্রের ফোল্ডারের নাম না আসে। আসল project-এ URL হতো https://github.com/example/lending.git-এর মতো।

Commit message-এর নিয়ম

দলটি Conventional Commits 1.0.0 মানে (পর্ব ১৮, ভালো commit, tag আর release): type(scope): description, দরকারে body, আর শেষে footer যেমন Fixes #7. এই project-এ যে type-গুলো এসেছে:

Typeকখনএই project-এ উদাহরণSemVer-এ প্রভাব
featনতুন আচরণfeat(fine): charge 5 taka per late dayminor
fixbug-এর সমাধানfix(fine): never charge a negative finepatch
testশুধু testtest(report): cover the due-date boundaryনেই
refactorআচরণ না বদলে গঠন বদলrefactor(lending): extract days_late helperনেই
docs, ci, choreলেখা, pipeline, রক্ষণাবেক্ষণchore(release): 1.0.0নেই

৫ · ধাপ ৫ — TDD দিয়ে core: lending.py

প্রতিটি story-তে একই ছন্দ — red, green, refactor (পর্ব ৪৮, TDD আর BDD). আগে একটি ব্যর্থ test, যা story-র AC-কে কোডের ভাষায় বলে; তারপর সবচেয়ে কম কোড যাতে test পাস করে; তারপর দরকার হলে পরিচ্ছন্নতা, test সবুজ রেখে। Rahim নিজের branch খুলে S1 আর S2-এর test লিখলেন — Arrange-Act-Assert ধাঁচে (পর্ব ৪৬, Unit test):

# test_borrow.py (first version)
import unittest
from datetime import date

from lending import Library

DAY = date(2026, 2, 2)


class BorrowTest(unittest.TestCase):
    def test_borrowed_book_is_on_loan(self):
        lib = Library()
        lib.borrow("B1", "M1", DAY)
        self.assertEqual(lib.loans["B1"], ("M1", DAY))

    def test_returned_book_is_no_longer_on_loan(self):
        lib = Library()
        lib.borrow("B1", "M1", DAY)
        lib.give_back("B1", DAY)
        self.assertNotIn("B1", lib.loans)

তারপর চালালেন — lending.py-তে তখনো শুধু LOAN_DAYS, তাই লাল। এরপর Library class লিখে আবার চালালেন, সবুজ, আর তখন commit:

$ git switch -c feature/borrow
Switched to a new branch 'feature/borrow'
$ python -m unittest
E
======================================================================
ERROR: test_borrow (unittest.loader._FailedTest.test_borrow)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_borrow
Traceback (most recent call last):
…
ImportError: cannot import name 'Library' from 'lending' (…\rahim\lending.py)


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
$ python -m unittest -v
test_borrowed_book_is_on_loan (test_borrow.BorrowTest.test_borrowed_book_is_on_loan) ... ok
test_returned_book_is_no_longer_on_loan (test_borrow.BorrowTest.test_returned_book_is_no_longer_on_loan) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
$ git add lending.py test_borrow.py
$ git commit -m "feat(borrow): borrow and return books" -m "Covers stories S1 and S2: a member borrows a book and returns it."
[feature/borrow 86970bb] feat(borrow): borrow and return books
 2 files changed, 32 insertions(+)
 create mode 100644 test_borrow.py
$ git push -u origin feature/borrow
To ../origin.git
 * [new branch]      feature/borrow -> feature/borrow
branch 'feature/borrow' set up to track 'origin/feature/borrow'.

লাল ধাপের কারণ ImportError — যে নাম test খুঁজছে, তা নেই। Beck-এর Test-Driven Development: By Example (২০০২) অনুযায়ী এটিও বৈধ লাল: “compile না হওয়া” মানেই ব্যর্থ test. Traceback-এর মাঝের লাইনগুলো (Python-এর ভেতরের path) … দিয়ে বাদ দেওয়া হয়েছে; “0.000s” যন্ত্রভেদে আলাদা হতে পারে।

একটি সচেতন সিদ্ধান্ত

লাল অবস্থাটি commit করা হয়নি — test আর কোড একসাথে, সবুজ হওয়ার পরে একটি commit. কারণ NFR-2: main-এর প্রতিটি commit সবুজ। লাল commit ইতিহাসে থাকলে পরে git bisect সেখানে পৌঁছে ভুল উত্তর দিতে পারে (অংশ ৮)। লাল test আলাদা commit করতে চাইলে ব্যক্তিগত branch-এ করুন, merge-এর আগে squash.

Nasrin একই সময়ে, একই base 4a99fdc থেকে, নিজের branch feature/fine-এ S4-এর দুটি AC নিয়ে একই ছন্দে কাজ করলেন: লাল (cannot import name 'fine'), তারপর সবুজ, তারপর commit 92d14d8 feat(fine): charge 5 taka per late day আর push. তাঁর test দুটি:

# test_fine.py (first version)
import unittest
from datetime import date, timedelta

from lending import fine

DAY = date(2026, 2, 2)


class FineTest(unittest.TestCase):
    def test_return_on_due_date_is_free(self):
        self.assertEqual(fine(DAY, DAY + timedelta(days=14)), 0)

    def test_three_days_late_costs_15_taka(self):
        self.assertEqual(fine(DAY, DAY + timedelta(days=17)), 15)

এখানে পর্ব ৪৭-এর (Test case নকশা) চোখে দেখুন। জরিমানার input-কে তিনটি equivalence partition-এ ভাগ করা যায়: মেয়াদের আগে (১৪-এর কম দিন), ঠিক মেয়াদে, আর দেরিতে; সীমা ১৪ আর ১৫ (boundary value). দুটি test দুটি partition ছুঁয়েছে — “আগে ফেরত” partition-এ কোনো test নেই। Line coverage তবু ১০০%, কারণ fine()-এর দুটি লাইনই চলেছে (পর্ব ৫০, Coverage আর complexity). Coverage বলে কোন লাইন চলেছে; কোন আচরণ যাচাই হয়েছে, বলে না।

চূড়ান্ত module আর তার test — নিজে চালিয়ে দেখুন

নিচে v1.0.1-এ tag করা lending.py-এর পুরো যুক্তি (শুধু ফাঁকা লাইন কমানো), আর পাঁচটি story-র AC থেকে বানানো test — S4-এর “আগে ফেরত” test-টি v1.0.1-এ যোগ হয়েছিল, কেন, অংশ ৮-এ দেখবেন। Test-গুলো sys.stdout-এ ফল লেখে, তাই program পরিষ্কারভাবে শেষ হয়:

import sys, unittest
from datetime import date, timedelta
# --- lending.py as tagged v1.0.1 (blank lines squeezed to fit the page)
LOAN_DAYS = 14
FINE_PER_DAY = 5  # taka
def days_late(borrowed_on, today):
    return (today - borrowed_on).days - LOAN_DAYS
def fine(borrowed_on, returned_on):
    return max(0, days_late(borrowed_on, returned_on)) * FINE_PER_DAY
class Library:
    def __init__(self):
        self.loans = {}  # book -> (member, borrowed_on)
    def borrow(self, book, member, day):
        if book in self.loans:
            raise ValueError(f"{book} is already borrowed")
        self.loans[book] = (member, day)
    def give_back(self, book, day):
        _, borrowed_on = self.loans.pop(book)
        return fine(borrowed_on, day)
    def overdue(self, today):
        return sorted(book for book, (_, day) in self.loans.items()
                      if days_late(day, today) > 0)
# --- the acceptance criteria as tests (condensed from test_*.py)
D = date(2026, 2, 2)
class Lending(unittest.TestCase):
    def setUp(self): self.lib = Library(); self.lib.borrow("B1", "M1", D)
    def test_s3_book_on_loan_cannot_be_borrowed(self):
        with self.assertRaises(ValueError): self.lib.borrow("B1", "M2", D)
    def test_s2_returned_book_leaves_loans(self):
        self.lib.give_back("B1", D); self.assertNotIn("B1", self.lib.loans)
    def test_s4_three_days_late_costs_15(self): self.assertEqual(self.lib.give_back("B1", D + timedelta(17)), 15)
    def test_s4_early_return_is_free(self): self.assertEqual(self.lib.give_back("B1", D + timedelta(10)), 0)
    def test_s5_due_date_is_not_overdue(self): self.assertEqual(self.lib.overdue(D + timedelta(14)), [])
    def test_s5_next_day_is_overdue(self): self.assertEqual(self.lib.overdue(D + timedelta(15)), ["B1"])
unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(Lending))

আউটপুট:

test_s2_returned_book_leaves_loans (__main__.Lending.test_s2_returned_book_leaves_loans) ... ok
test_s3_book_on_loan_cannot_be_borrowed (__main__.Lending.test_s3_book_on_loan_cannot_be_borrowed) ... ok
test_s4_early_return_is_free (__main__.Lending.test_s4_early_return_is_free) ... ok
test_s4_three_days_late_costs_15 (__main__.Lending.test_s4_three_days_late_costs_15) ... ok
test_s5_due_date_is_not_overdue (__main__.Lending.test_s5_due_date_is_not_overdue) ... ok
test_s5_next_day_is_overdue (__main__.Lending.test_s5_next_day_is_overdue) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK

unittest test-গুলো নামের বর্ণানুক্রমে চালায়, লেখার ক্রমে নয়; তাই প্রতিটির আগে setUp একটি নতুন Library বানিয়ে B1 ধার দেয় (একটি fixture). নামের শুরুতে story-র ID থাকায় traceability এক নজরে। ১৭ দিনে ৩ দিন দেরি, ৩ × ৫ = ১৫ টাকা; ১০ দিনে days_late −৪, কিন্তু max(0, −4) = ০ — ঠিক এই max-টিই একদিন হারিয়েছিল। সময়ের অঙ্ক যন্ত্রভেদে আলাদা হতে পারে।

আর বাস্তব repository-তে test-গুলো তিনটি file-এ (test_borrow.py, test_fine.py, test_report.py), যা python -m unittest নিজে খুঁজে নেয় (discovery: test*.py নামের file). Unit test-ই এখানে যথেষ্ট, কারণ module-টির বাইরের কোনো নির্ভরতা নেই — database বা network এলে test double আর integration test লাগত (পর্ব ৪৯, Test double; পর্ব ৫১, Test pyramid).

৬ · ধাপ ৬ — দুজনের কাজ: PR review, conflict, git rebase main আর --no-ff

এখন দুটি feature branch একই commit 4a99fdc থেকে বেরিয়েছে, আর দুজনেই lending.py-এর শেষে কোড যোগ করেছেন — Rahim Library class, Nasrin FINE_PER_DAY আর fine(). প্রথমটি merge হলে দ্বিতীয়টি conflict দেখাবেই। নিচের stepper-এ দুটি PR-এর review থেকে merge পর্যন্ত; মন্তব্যগুলো Conventional Comments-এর লেবেলে (issue, praise, question…), যা পর্ব ৪৪-এ (Code review) দেখেছেন।

PR #1 · feature/borrow → mainRahim, ২ ফেব্রু ১১:০৫
feat(borrow): borrow and return books
Stories: S1, S2
2 files changed: lending.py, test_borrow.py (+32)
Checks: ci / test  ✔ passed
Reviewers: Nasrin (requested)
Nasrin-এর review৩ ফেব্রু ১৫:০০ · changes requested
issue (blocking): lending.py line 11 rejects a book that
  is on loan (S3), but no test covers it. Please add one.
praise: test names read like the acceptance criteria.
question (non-blocking): give_back on an unknown book
  raises KeyError - should README say so?
Rahim-এর উত্তর আর merge৪ ফেব্রু ০৯:৩০ → ১০:১০
e311026 test(borrow): reject borrowing a book that is on loan
Nasrin: approved (10:00)
$ git merge --no-ff feature/borrow -m "Merge pull request #1 ..."
Merge made by the 'ort' strategy.
main: 4a99fdc -> 63acace
PR #2 · feature/fine → mainRahim-এর review, ৪ ফেব্রু ১১:০০
This branch has conflicts that must be resolved: lending.py
issue (blocking): S4 AC3 says the fine is known at return
  time, but give_back still returns nothing.
suggestion: rebase on main first, then add AC3 as its own
  commit so the conflict fix stays small.
git rebase main — conflictNasrin, ৪ ফেব্রু ১৪:০০
LOAN_DAYS = 14
<<<<<<< HEAD          (main + replayed commits)
class Library: ...           (Rahim's code)
=======
FINE_PER_DAY = 5  # taka     (Nasrin's 92d14d8)
def fine(...): ...
>>>>>>> 92d14d8 (feat(fine): charge 5 taka per late day)
সমাধান: দুই অংশই থাকবে১৪:১০
LOAN_DAYS = 14
FINE_PER_DAY = 5  # taka
def fine(borrowed_on, returned_on): ...
class Library: ...
$ python -m unittest      -> Ran 5 tests ... OK
$ git add lending.py
$ git rebase --continue   -> 92d14d8 becomes cadacb4
AC3, push আর merge৪ ফেব্রু ১৪:৪০ → ৫ ফেব্রু ১০:১৫
4db12a0 feat(fine): return the fine from give_back
$ git push                 -> rejected (non-fast-forward)
$ git push --force-with-lease  -> forced update
Rahim: approved (5 Feb 10:00)
main: 63acace -> 609f70f (Merge pull request #2)
Branch আর PR-এর অবস্থা
main
feature/fine
PR #1
PR #2
  1. PR #1 খোলা. Rahim push করে PR খুললেন। বিবরণে কোন story, কী বদলেছে, আর CI সবুজ। Review-এর অনুরোধ Nasrin-কে।
  2. Review (২৮ ঘণ্টা পরে). Nasrin একটি blocking issue পেলেন: S3-এর কোড আছে, test নেই — অর্থাৎ ওই অংশ TDD-তে লেখা হয়নি। সাথে একটি প্রশংসা আর একটি non-blocking প্রশ্ন। দেরিটিও লক্ষ করুন; retrospective-এ ফিরে আসবে।
  3. উত্তর আর merge. Rahim একটি test commit যোগ করলেন, Nasrin approve করলেন, Rahim --no-ff দিয়ে merge করলেন — main এখন 63acace.
  4. PR #2-এ conflict. main এগিয়ে যাওয়ায় platform conflict দেখাল। Rahim-এর review-এ একটি blocking issue (AC3 অসম্পূর্ণ) আর একটি পরামর্শ: আগে rebase, তারপর AC3 আলাদা commit-এ — যাতে conflict-এর সমাধান ছোট আর পরিষ্কার থাকে।
  5. Rebase-এ conflict. Rebase-এর সময় HEAD হলো নতুন base (main) আর ইতিমধ্যে বসানো commit, আর নিচের অংশ Nasrin-এর replay হতে থাকা commit — merge-এর ঠিক উল্টো (পর্ব ১৫, Rebase).
  6. সমাধান. দুটি যোগই রাখা: নিয়ম আর fine() ওপরে, Library নিচে। Test চালিয়ে (৫টি সবুজ) তবেই git add আর --continue. নতুন parent-এর ওপর বসায় commit-টির hash বদলে cadacb4.
  7. AC3 আর merge. নতুন commit 4db12a0. Branch-এর ইতিহাস বদলেছে, তাই সাধারণ push প্রত্যাখ্যাত; --force-with-lease নিরাপদে চাপিয়ে দেয়। Approval-এর পরে Nasrin merge করলেন — main 609f70f.

Review-এর পরের অংশ, Rahim-এর যন্ত্রে — test যোগ, push, তারপর merge আর branch মুছে ফেলা:

$ python -m unittest
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
$ git add test_borrow.py
$ git commit -m "test(borrow): reject borrowing a book that is on loan" -m "Story S3 had code but no test (review comment on PR #1)."
[feature/borrow e311026] test(borrow): reject borrowing a book that is on loan
 1 file changed, 6 insertions(+)
$ git push
To ../origin.git
   86970bb..e311026  feature/borrow -> feature/borrow
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
From ../origin
 * [new branch]      feature/fine -> origin/feature/fine
Already up to date.
$ git merge --no-ff feature/borrow -m "Merge pull request #1 from feature/borrow"
Merge made by the 'ort' strategy.
 lending.py     | 13 +++++++++++++
 test_borrow.py | 25 +++++++++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 test_borrow.py
$ git push
To ../origin.git
   4a99fdc..63acace  main -> main
$ git branch -d feature/borrow
Deleted branch feature/borrow (was e311026).
$ git push origin --delete feature/borrow
To ../origin.git
 - [deleted]         feature/borrow

--no-ff fast-forward সম্ভব হলেও একটি merge commit বানায় (পর্ব ১৩, Merge আর conflict); এই দলে প্রতিটি merge commit = একটি PR-এর রেকর্ড।

এবার Nasrin-এর দিক — পুরো rebase session, কিছু বাদ না দিয়ে:

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
From ../origin
   4a99fdc..63acace  main       -> origin/main
Updating 4a99fdc..63acace
Fast-forward
 lending.py     | 13 +++++++++++++
 test_borrow.py | 25 +++++++++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 test_borrow.py
$ git switch feature/fine
Switched to branch 'feature/fine'
Your branch is up to date with 'origin/feature/fine'.
$ git rebase main
Rebasing (1/1)
Auto-merging lending.py
CONFLICT (content): Merge conflict in lending.py
error: could not apply 92d14d8... feat(fine): charge 5 taka per late day
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply 92d14d8... # feat(fine): charge 5 taka per late day
$ git status
interactive rebase in progress; onto 63acace
Last command done (1 command done):
   pick 92d14d8 # feat(fine): charge 5 taka per late day
No commands remaining.
You are currently rebasing branch 'feature/fine' on '63acace'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   test_fine.py

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
	both modified:   lending.py

$ cat lending.py
"""Library lending core (fictional demo project)."""

LOAN_DAYS = 14
<<<<<<< HEAD


class Library:
    def __init__(self):
        self.loans = {}  # book -> (member, borrowed_on)

    def borrow(self, book, member, day):
        if book in self.loans:
            raise ValueError(f"{book} is already borrowed")
        self.loans[book] = (member, day)

    def give_back(self, book, day):
        self.loans.pop(book)
=======
FINE_PER_DAY = 5  # taka


def fine(borrowed_on, returned_on):
    late = (returned_on - borrowed_on).days - LOAN_DAYS
    return max(0, late) * FINE_PER_DAY
>>>>>>> 92d14d8 (feat(fine): charge 5 taka per late day)
$ python -m unittest
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
$ git add lending.py
$ git rebase --continue
[detached HEAD cadacb4] feat(fine): charge 5 taka per late day
 2 files changed, 20 insertions(+)
 create mode 100644 test_fine.py
Successfully rebased and updated refs/heads/feature/fine.
$ git log --oneline --graph -5
* cadacb4 feat(fine): charge 5 taka per late day
*   63acace Merge pull request #1 from feature/borrow
|\  
| * e311026 test(borrow): reject borrowing a book that is on loan
| * 86970bb feat(borrow): borrow and return books
|/  
* 4a99fdc ci: add lint and test workflow
$ python -m unittest
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK
$ git add lending.py test_fine.py
$ git commit -m "feat(fine): return the fine from give_back" -m "Acceptance criterion of S4 (review comment on PR #2)."
[feature/fine 4db12a0] feat(fine): return the fine from give_back
 2 files changed, 8 insertions(+), 2 deletions(-)
$ git push
To ../origin.git
 ! [rejected]        feature/fine -> feature/fine (non-fast-forward)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../origin.git
 + 92d14d8...4db12a0 feature/fine -> feature/fine (forced update)

পড়ার মতো কয়েকটি জিনিস। ১. Rebase replay করে শুধু একটি commit (pick 92d14d8); নতুন test_fine.py নির্বিঘ্নে বসেছে, শুধু lending.py “both modified”. ২. দুজনেই LOAN_DAYS = 14-এর পরে লাইন যোগ করেছেন; Git অর্থ বোঝে না, সমাধানের সিদ্ধান্ত মানুষের (পর্ব ৫৪, Merge-এর ভেতরে)। ৩. সমাধানে নতুন আচরণ ঢোকানো হয়নি — give_back-এর জরিমানা আলাদা commit-এ, যাতে review-এ দুটি আলাদা দেখা যায়। ৪. সাধারণ push প্রত্যাখ্যাত, কারণ server-এর branch এখনো পুরোনো 92d14d8-এ। --force-with-lease শুধু তখনই চাপায়, যখন server-এর branch এখনো সেই commit-এ, যা Nasrin শেষবার দেখেছিলেন; সাধারণ --force সেই প্রশ্নই করে না। আর এটি শুধু নিজের feature branch-এ — main-এ force push নিষেধ।

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
Already up to date.
$ git merge --no-ff feature/fine -m "Merge pull request #2 from feature/fine"
Merge made by the 'ort' strategy.
 lending.py   |  9 ++++++++-
 test_fine.py | 19 +++++++++++++++++++
 2 files changed, 27 insertions(+), 1 deletion(-)
 create mode 100644 test_fine.py
$ git push
To ../origin.git
   63acace..609f70f  main -> main
$ git branch -d feature/fine
Deleted branch feature/fine (was 4db12a0).
$ git push origin --delete feature/fine
To ../origin.git
 - [deleted]         feature/fine

Rebase আগেই conflict মিটিয়েছে, তাই এখানে conflict নেই। ফল: প্রতিটি PR-এর commit-গুলো একটি সরল “বুদবুদ”-এর ভেতরে, আর main-এর first-parent ইতিহাস হলো শুধু PR-এর তালিকা — git log --first-parent --oneline main. এই দুটি সুবিধা একসাথে পেতেই CONTRIBUTING-এর নিয়ম ৫: “rebase, তারপর --no-ff”.

Sprint ২-এ একই ছন্দে আরও চারটি PR: #3 feature/overdue (S5), #4 refactor/days-late (PR #3-এর review-এ Nasrin লিখেছিলেন “১৪ দিনের নিয়ম দুবার লেখা”), #5 docs/usage আর #6 release/1.0.0 — প্রতিটি সর্বশেষ main থেকে শুরু, তাই conflict নেই।

৭ · ধাপ ৭ ও ৮ — CI pipeline আর release: YAML, tag, CHANGELOG আর SemVer

Repository-র দ্বিতীয় commit-ই ছিল CI (পর্ব ৫৮, Continuous integration). প্রথম দিন থেকে pipeline থাকলে প্রতিটি PR-এর প্রথম push-ই যাচাই হয়, আর “পরে যোগ করব” কখনো আসে না। GitHub Actions-এর জন্য লেখা workflow (উদাহরণ; action-গুলোর version লেখার সময়ের, চালানোর আগে action-এর repository-তে মিলিয়ে নিন):

# .github/workflows/ci.yml  (GitHub Actions)
name: ci
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.14"
      - name: lint
        run: |
          python -m pip install "ruff==0.9.0"
          ruff check .
      - name: unit tests
        run: python -m unittest discover -v
  • Trigger: main-মুখী প্রতিটি PR, আর main-এ প্রতিটি push (merge-এর পরে আবার যাচাই)।
  • ন্যূনতম অনুমতি: contents: read — এই job কিছু লেখে না, তাই token-ও লিখতে পারে না।
  • Lint আগে: linter ruff সস্তা আর দ্রুত; version pin করা, যাতে নতুন নিয়মে হঠাৎ build লাল না হয়।
  • Test: ঠিক সেই command, যা README-তে লেখা আর developer নিজের যন্ত্রে চালান — CI আর local একই।
  • সময়সীমা: timeout-minutes: 10; NFR-2-এর লক্ষ্য ৫ মিনিট।

সৎভাবে বলি: transcript-গুলো একটি স্থানীয় bare repository-তে, তাই GitHub-এর runner বা ruff এখানে চলেনি; test অংশটি (python -m unittest) প্রতিটি PR-এর আগে স্থানীয়ভাবে চালানো, যা transcript-এ দেখছেন।

Release v1.0.0 — CHANGELOG, SemVer আর annotated tag

S5 শেষ হওয়ার পর লাইব্রেরিয়ান বললেন, পাঠাগারে এবার ব্যবহার শুরু হোক। SemVer 2.0.0 অনুযায়ী 1.0.0 মানে প্রকাশ্য API — এখানে Library-র তিনটি method আর fine() — এখন থেকে স্থির: ভাঙা পরিবর্তন হলে MAJOR, নতুন সুবিধা হলে MINOR, শুধু bug-এর সমাধান হলে PATCH (পর্ব ১৮)। Rahim একটি release PR খুললেন, শুধু CHANGELOG নিয়ে (Keep a Changelog ধাঁচ: মানুষের জন্য লেখা, commit-এর তালিকা নয়):

# CHANGELOG.md (in v1.0.0)
# Changelog

All notable changes are listed here (Keep a Changelog style).
This project follows Semantic Versioning 2.0.0.

## [1.0.0] - 2026-02-12

### Added
- Borrow and return books; a book on loan cannot be borrowed twice.
- Overdue fine: 5 taka per day after 14 days.
- Overdue list for the librarian, sorted by book id.
$ git switch -c release/1.0.0
Switched to a new branch 'release/1.0.0'
$ git add CHANGELOG.md
$ git commit -m "chore(release): 1.0.0"
[release/1.0.0 1aee4eb] chore(release): 1.0.0
 1 file changed, 11 insertions(+)
 create mode 100644 CHANGELOG.md
$ git push -u origin release/1.0.0
To ../origin.git
 * [new branch]      release/1.0.0 -> release/1.0.0
branch 'release/1.0.0' set up to track 'origin/release/1.0.0'.
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
Already up to date.
$ git merge --no-ff release/1.0.0 -m "Merge pull request #6 from release/1.0.0"
Merge made by the 'ort' strategy.
 CHANGELOG.md | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 CHANGELOG.md
$ git tag -a v1.0.0 -m "lending 1.0.0"
$ git push --follow-tags
To ../origin.git
   516d42b..60c8dab  main -> main
 * [new tag]         v1.0.0 -> v1.0.0
$ git describe
v1.0.0
$ git branch -d release/1.0.0
Deleted branch release/1.0.0 (was 1aee4eb).
$ git push origin --delete release/1.0.0
To ../origin.git
 - [deleted]         release/1.0.0
$ git log --graph --oneline -12
*   60c8dab Merge pull request #6 from release/1.0.0
|\  
| * 1aee4eb chore(release): 1.0.0
|/  
*   516d42b Merge pull request #5 from docs/usage
|\  
| * 27ca69e docs(readme): add a usage example
|/  
*   af97afe Merge pull request #4 from refactor/days-late
|\  
| * 1f8d3cb test(report): cover the due-date boundary
| * 72cb901 refactor(lending): extract days_late helper
|/  
*   1bebbf6 Merge pull request #3 from feature/overdue
|\  
| * 63bd2c0 feat(report): list overdue loans
|/  
*   609f70f Merge pull request #2 from feature/fine
|\  
| * 4db12a0 feat(fine): return the fine from give_back
| * cadacb4 feat(fine): charge 5 taka per late day

Tag দেওয়া হয়েছে merge commit-এ (60c8dab), branch-এর commit-এ নয় — কারণ release হলো main-এর একটি অবস্থা। Annotated tag (-a) নিজেই একটি Git object, যাতে tagger-এর নাম, সময় আর বার্তা থাকে; lightweight tag শুধু একটি নাম। --follow-tags push-এর সাথে নতুন annotated tag-ও পাঠায়। আর git describe বলছে HEAD ঠিক v1.0.0-এ।

নিচের ছবিটি project শেষে git log --graph --oneline --all-এর ফল থেকে আঁকা — আসল output-টি ছবির পরে:

চূড়ান্ত commit graph — main, সাতটি PR-এর branch, merge আর দুটি tag

নতুন commit ওপরে · নীল = main-এর merge · বেগুনি = Rahim · কমলা = Nasrin · লাল বৃত্ত = bug

lending repository-র চূড়ান্ত commit graph 60b78d1 Merge #8 5b3fc4f chore(release): 1.0.1 da9e35e fix(fine): no negative 60c8dab Merge #6 1aee4eb chore(release): 1.0.0 516d42b Merge #5 · docs/usage 27ca69e docs(readme): usage af97afe Merge #4 · refactor/days-late 1f8d3cb test(report): due date 72cb901 refactor: days_late ✗ bug 1bebbf6 Merge #3 · feature/overdue 63bd2c0 feat(report): overdue 609f70f Merge #2 · feature/fine 4db12a0 feat(fine): from give_back cadacb4 feat(fine): 5 taka/day 63acace Merge #1 · feature/borrow e311026 test(borrow): double borrow 86970bb feat(borrow): borrow, return 4a99fdc ci: lint and test workflow a323299 chore: initial skeleton v1.0.1 HEAD → main v1.0.0 main / merge Rahim Nasrin bug
ছবি ৬৫.২ — ২০টি commit: ৭টি merge, ১৩টি সাধারণ। প্রতিটি branch একটি merge-এর বুদবুদ, আর main-এর বাঁয়ের সরলরেখা (first parent) শুধু PR-গুলোর ক্রম। PR #7 নেই, কারণ #7 নম্বরটি bug report-এর issue পেয়েছিল — অনেক platform-এ issue আর PR একই নম্বরের ধারা ভাগ করে। Nasrin-এর পুরোনো 92d14d8 rebase-এর পরে কোনো branch থেকে পৌঁছানো যায় না, তাই --all-এও নেই (reflog-এ কিছুদিন থাকে, পর্ব ৫৩)।
$ git log --graph --oneline --all
*   60b78d1 Merge pull request #8 from fix/negative-fine
|\  
| * 5b3fc4f chore(release): 1.0.1
| * da9e35e fix(fine): never charge a negative fine
|/  
*   60c8dab Merge pull request #6 from release/1.0.0
|\  
| * 1aee4eb chore(release): 1.0.0
|/  
*   516d42b Merge pull request #5 from docs/usage
|\  
| * 27ca69e docs(readme): add a usage example
|/  
*   af97afe Merge pull request #4 from refactor/days-late
|\  
| * 1f8d3cb test(report): cover the due-date boundary
| * 72cb901 refactor(lending): extract days_late helper
|/  
*   1bebbf6 Merge pull request #3 from feature/overdue
|\  
| * 63bd2c0 feat(report): list overdue loans
|/  
*   609f70f Merge pull request #2 from feature/fine
|\  
| * 4db12a0 feat(fine): return the fine from give_back
| * cadacb4 feat(fine): charge 5 taka per late day
|/  
*   63acace Merge pull request #1 from feature/borrow
|\  
| * e311026 test(borrow): reject borrowing a book that is on loan
| * 86970bb feat(borrow): borrow and return books
|/  
* 4a99fdc ci: add lint and test workflow
* a323299 chore: initial project skeleton
$ git tag -n
v1.0.0          lending 1.0.0
v1.0.1          lending 1.0.1

৮ · ধাপ ৯ — Release-এর পরে: bug report → bisect → regression test → v1.0.1

রবিবার, ১৫ ফেব্রুয়ারি, সকাল ৯টা। লাইব্রেরিয়ান issue tracker-এ (issue tracker) একটি অভিযোগ লিখলেন। একটি ভালো bug report-এর চেহারা এমন — ধাপ, প্রত্যাশা আর বাস্তব আলাদা:

#7  Early return shows a negative fine
Version : v1.0.0
Steps   : B7 borrowed on 2 Feb, returned on 12 Feb (10 days)
Expected: fine 0 taka (returned before the due date)
Actual  : receipt shows fine -20 taka
Impact  : wrong money on receipts; members confused
Severity: high (money), workaround: staff ignore negative values

প্রথম কাজ: অভিযোগকে একটি স্বয়ংক্রিয় প্রশ্নে রূপ দেওয়া। Rahim একটি ছোট regression test লিখলেন, repository-র বাইরে (../regress/test_issue7.py), যাতে পুরোনো commit checkout করলেও test-টি হারিয়ে না যায় বা বদলে না যায় (পর্ব ৫৫, Git দিয়ে bug খোঁজা):

import unittest
from datetime import date, timedelta

from lending import fine


class Issue7(unittest.TestCase):
    def test_early_return_is_free(self):
        day = date(2026, 2, 2)
        self.assertEqual(fine(day, day + timedelta(days=10)), 0)

তারপর দুই প্রান্ত যাচাই: এখনকার main-এ test ব্যর্থ হওয়া উচিত, আর এমন একটি পুরোনো commit-এ পাস করা উচিত, যেখানে জরিমানার কোড ছিল। 609f70f (PR #2-এর merge, যেখানে fine() প্রথম main-এ এল) স্বাভাবিক প্রার্থী:

$ python -m unittest discover -s ../regress
F
======================================================================
FAIL: test_early_return_is_free (test_issue7.Issue7.test_early_return_is_free)
----------------------------------------------------------------------
Traceback (most recent call last):
…
AssertionError: -20 != 0

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
$ git switch --detach 609f70f
HEAD is now at 609f70f Merge pull request #2 from feature/fine
$ python -m unittest discover -s ../regress
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
$ git switch main
Previous HEAD position was 609f70f Merge pull request #2 from feature/fine
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

এখন git bisect run: Git নিজেই মাঝামাঝি commit checkout করবে, command চালাবে, আর exit code দেখে রায় দেবে — ০ মানে ভালো, ১–১২৭ (১২৫ বাদে) মানে খারাপ, ১২৫ মানে “পরীক্ষা করা যায় না, skip”. unittest ব্যর্থ হলে ১ দিয়ে বেরোয়, তাই আলাদা script লাগে না:

$ git bisect start v1.0.0 609f70f
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[af97afe2bdbfa5c8252095772dbf8788f505a79b] Merge pull request #4 from refactor/days-late
$ git bisect run python -m unittest discover -s ../regress
running 'python' '-m' 'unittest' 'discover' '-s' '../regress'
F
======================================================================
FAIL: test_early_return_is_free (test_issue7.Issue7.test_early_return_is_free)
----------------------------------------------------------------------
Traceback (most recent call last):
…
AssertionError: -20 != 0

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
Bisecting: 2 revisions left to test after this (roughly 1 step)
[1bebbf6a53686e2ab52dae4262c378af5ba4fb7a] Merge pull request #3 from feature/overdue
running 'python' '-m' 'unittest' 'discover' '-s' '../regress'
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Bisecting: 0 revisions left to test after this (roughly 1 step)
[1f8d3cb6b507a1556694de11b0db30392384bd21] test(report): cover the due-date boundary
running 'python' '-m' 'unittest' 'discover' '-s' '../regress'
F
======================================================================
FAIL: test_early_return_is_free (test_issue7.Issue7.test_early_return_is_free)
----------------------------------------------------------------------
Traceback (most recent call last):
…
AssertionError: -20 != 0

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[72cb9012a7b9866d170d303871dc1682b2b4ccaa] refactor(lending): extract days_late helper
running 'python' '-m' 'unittest' 'discover' '-s' '../regress'
F
======================================================================
FAIL: test_early_return_is_free (test_issue7.Issue7.test_early_return_is_free)
----------------------------------------------------------------------
Traceback (most recent call last):
…
AssertionError: -20 != 0

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
72cb9012a7b9866d170d303871dc1682b2b4ccaa is the first bad commit
commit 72cb9012a7b9866d170d303871dc1682b2b4ccaa
Author: Rahim <rahim@example.com>
Date:   Tue Feb 10 10:30:00 2026 +0600

    refactor(lending): extract days_late helper
    
    fine() and overdue() repeated the 14-day rule (review note on PR #3).

 lending.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
bisect found first bad commit
$ git bisect log
# bad: [60c8dabf100ce0eace81e68950a3eeff501799bc] Merge pull request #6 from release/1.0.0
# good: [609f70fb0536d5453a5252e24ca5a1a5696719b3] Merge pull request #2 from feature/fine
git bisect start 'v1.0.0' '609f70f'
# bad: [af97afe2bdbfa5c8252095772dbf8788f505a79b] Merge pull request #4 from refactor/days-late
git bisect bad af97afe2bdbfa5c8252095772dbf8788f505a79b
# good: [1bebbf6a53686e2ab52dae4262c378af5ba4fb7a] Merge pull request #3 from feature/overdue
git bisect good 1bebbf6a53686e2ab52dae4262c378af5ba4fb7a
# bad: [1f8d3cb6b507a1556694de11b0db30392384bd21] test(report): cover the due-date boundary
git bisect bad 1f8d3cb6b507a1556694de11b0db30392384bd21
# bad: [72cb9012a7b9866d170d303871dc1682b2b4ccaa] refactor(lending): extract days_late helper
git bisect bad 72cb9012a7b9866d170d303871dc1682b2b4ccaa
# first bad commit: [72cb9012a7b9866d170d303871dc1682b2b4ccaa] refactor(lending): extract days_late helper
$ git bisect reset
Previous HEAD position was 72cb901 refactor(lending): extract days_late helper
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

চারবার চালিয়ে উত্তর — দোষী 72cb901, Rahim-এর নিজের refactor. Traceback-এর ভেতরের path-এর লাইন … দিয়ে বাদ দেওয়া হয়েছে; দেখুন, প্রতিবার একই AssertionError: -20 != 0. নিচের stepper-এ পরিসর কীভাবে ছোট হলো:

960c8dab Merge #6  (v1.0.0, bad)81aee4eb chore(release): 1.0.07516d42b Merge #5627ca69e docs(readme): usage5af97afe Merge #441f8d3cb test(report): due date372cb901 refactor: days_late21bebbf6 Merge #3163bd2c0 feat(report): overdue0609f70f Merge #2  (good)
Bisect-এর অবস্থা
সন্দেহভাজন
পরীক্ষা
Git-এর বার্তা
  1. শুরু. v1.0.0 খারাপ, 609f70f ভালো। সন্দেহের মধ্যে ভালো-র পরের নয়টি commit (merge-সহ)। Git মাঝামাঝি af97afe বেছে নেয় — এর পরে সর্বোচ্চ ৩টি বাকি থাকবে, “roughly 2 steps”.
  2. af97afe খারাপ. PR #4-এর merge-এ −২০. তাই ৬–৯ বাদ (সেগুলো এর পরের, খারাপ হবেই); সন্দেহ এর পূর্বপুরুষ ১–৫. পরের পরীক্ষা 1bebbf6.
  3. 1bebbf6 ভালো. PR #3-এর merge-এ জরিমানা ০ — অর্থাৎ overdue-র feature দোষী নয়; ১ আর ২ বাদ। বাকি ৩টি: PR #4-এর দুটি commit আর তার merge.
  4. 1f8d3cb খারাপ. শুধু test যোগ করা commit-এও −২০ — দোষ এর আগেই ঢুকেছে। বাকি ৩ আর ৪.
  5. 72cb901 খারাপ. এটি 1bebbf6-এর (ভালো) ঠিক পরের commit, আর নিজে খারাপ — তাই এটিই প্রথম খারাপ। মোট চারটি রায়; ⌈log₂ 9⌉ = ৪-এর সাথে মেলে।
  6. কারণ পড়া. git bisect reset দিয়ে main-এ ফেরা, তারপর diff: days_late() বের করার সময় max(0, …) পড়ে গেছে। Test সবুজ ছিল, কারণ কোনো test আগে-ফেরতের partition ছোঁয়নি।
$ git show 72cb901 -- lending.py
commit 72cb9012a7b9866d170d303871dc1682b2b4ccaa
Author: Rahim <rahim@example.com>
Date:   Tue Feb 10 10:30:00 2026 +0600

    refactor(lending): extract days_late helper
    
    fine() and overdue() repeated the 14-day rule (review note on PR #3).

diff --git a/lending.py b/lending.py
index eec4800..3ed686e 100644
--- a/lending.py
+++ b/lending.py
@@ -4,9 +4,12 @@ LOAN_DAYS = 14
 FINE_PER_DAY = 5  # taka
 
 
+def days_late(borrowed_on, today):
+    return (today - borrowed_on).days - LOAN_DAYS
+
+
 def fine(borrowed_on, returned_on):
-    late = (returned_on - borrowed_on).days - LOAN_DAYS
-    return max(0, late) * FINE_PER_DAY
+    return days_late(borrowed_on, returned_on) * FINE_PER_DAY
 
 
 class Library:
@@ -24,4 +27,4 @@ class Library:
 
     def overdue(self, today):
         return sorted(book for book, (_, day) in self.loans.items()
-                      if (today - day).days > LOAN_DAYS)
+                      if days_late(day, today) > 0)

Refactor-টির উদ্দেশ্য ভালো ছিল — একই নিয়ম দুবার না লেখা (DRY, পর্ব ২৬; পর্ব ৪২, Code smell আর refactoring). কিন্তু helper-এ একত্র করার সময় “ঋণাত্মক হলে ০” অংশটি helper-এর ভেতরে না বাইরে, সেই সিদ্ধান্তে ফাঁক রইল। Refactoring মানে আচরণ না বদলে গঠন বদল; এখানে আচরণ বদলেছে, আর কোনো test সেটি দেখেনি। Fowler-এর Refactoring (১৯৯৯; দ্বিতীয় সংস্করণ ২০১৮)-এর প্রথম শর্তই ভরসাযোগ্য test.

নিচের program-এ পাশাপাশি দুটি function — bug-ওয়ালা (72cb901-এর পরের) আর সংশোধিত (da9e35e-এর পরের) — আর একই regression suite দুটির ওপর:

import io, unittest
from datetime import date, timedelta
LOAN_DAYS, FINE_PER_DAY = 14, 5
def days_late(borrowed_on, today):
    return (today - borrowed_on).days - LOAN_DAYS

def fine_buggy(borrowed_on, returned_on):   # after 72cb901 (in v1.0.0)
    return days_late(borrowed_on, returned_on) * FINE_PER_DAY

def fine_fixed(borrowed_on, returned_on):   # after da9e35e (in v1.0.1)
    return max(0, days_late(borrowed_on, returned_on)) * FINE_PER_DAY

def regression_suite(fine):
    class Issue7(unittest.TestCase):         # the test from the bug report
        def test_early_return_is_free(self):
            d = date(2026, 2, 2)
            self.assertEqual(fine(d, d + timedelta(days=10)), 0)
        def test_old_behaviour_kept(self):      # 14 days free, 17 days 15 taka
            d = date(2026, 2, 2)
            self.assertEqual([fine(d, d + timedelta(days=n)) for n in (14, 17)], [0, 15])
    return unittest.defaultTestLoader.loadTestsFromTestCase(Issue7)

for f in (fine_buggy, fine_fixed):
    log = io.StringIO()
    r = unittest.TextTestRunner(stream=log).run(regression_suite(f))
    print(f"{f.__name__}: ran {r.testsRun}, failures {len(r.failures)}"
          f" -> {'PASS' if r.wasSuccessful() else 'FAIL'}")
    for test, tb in r.failures:
        print("   ", test.id().split(".")[-1], "|", tb.strip().splitlines()[-1])
print("fine for 3, 10, 14, 15, 20 days:",
      [fine_buggy(date(2026, 2, 2), date(2026, 2, 2) + timedelta(n)) for n in (3, 10, 14, 15, 20)],
      [fine_fixed(date(2026, 2, 2), date(2026, 2, 2) + timedelta(n)) for n in (3, 10, 14, 15, 20)])

আউটপুট:

fine_buggy: ran 2, failures 1 -> FAIL
    test_early_return_is_free | AssertionError: -20 != 0
fine_fixed: ran 2, failures 0 -> PASS
fine for 3, 10, 14, 15, 20 days: [-55, -20, 0, 5, 30] [0, 0, 0, 5, 30]

দুটি test: একটি bug-এর জন্য (১০ দিন → ০), একটি পুরোনো আচরণ রক্ষার জন্য (১৪ দিন → ০, ১৭ দিন → ১৫)। Bug-ওয়ালা version শুধু প্রথমটিতে ব্যর্থ — তাই পুরোনো test-গুলো এটি ধরেনি। সংশোধিত version দুটিতেই পাস: fix নতুন কিছু ভাঙেনি। শেষ লাইন পাঁচটি partition-এর মান পাশাপাশি দেখায়: ৩ দিনে −৫৫ টাকা (১১ দিন আগে × ৫), ১০ দিনে −২০, ১৪-তে দুটিতেই ০, ১৫ আর ২০-এ দুটিই সমান। অর্থাৎ পার্থক্য শুধু “আগে ফেরত” partition-এ — ঠিক যেখানে কোনো test ছিল না। প্রতিটি suite-এর ফল একটি StringIO-তে লেখা, তাই program-টি নিজে সবসময় সফলভাবে শেষ হয়।

এবার fix — নিজের branch-এ, TDD-র ছন্দে: আগে regression test-টি repository-র test_fine.py-তে (লাল), তারপর এক লাইনের সমাধান (সবুজ), তারপর CHANGELOG আর patch release (পর্ব ১১-এর ভাষায় এটি “ভুল শোধরানো”, কিন্তু ইতিহাস মুছে নয় — নতুন commit দিয়ে; ভুল শোধরানো):

$ git switch -c fix/negative-fine
Switched to a new branch 'fix/negative-fine'
$ python -m unittest
...F.....
======================================================================
FAIL: test_early_return_is_free (test_fine.FineTest.test_early_return_is_free)
----------------------------------------------------------------------
Traceback (most recent call last):
…
AssertionError: -20 != 0

----------------------------------------------------------------------
Ran 9 tests in 0.001s

FAILED (failures=1)
$ python -m unittest -v
test_book_on_loan_cannot_be_borrowed_again (test_borrow.BorrowTest.test_book_on_loan_cannot_be_borrowed_again) ... ok
test_borrowed_book_is_on_loan (test_borrow.BorrowTest.test_borrowed_book_is_on_loan) ... ok
test_returned_book_is_no_longer_on_loan (test_borrow.BorrowTest.test_returned_book_is_no_longer_on_loan) ... ok
test_early_return_is_free (test_fine.FineTest.test_early_return_is_free) ... ok
test_give_back_returns_the_fine (test_fine.FineTest.test_give_back_returns_the_fine) ... ok
test_return_on_due_date_is_free (test_fine.FineTest.test_return_on_due_date_is_free) ... ok
test_three_days_late_costs_15_taka (test_fine.FineTest.test_three_days_late_costs_15_taka) ... ok
test_due_date_itself_is_not_overdue (test_report.OverdueTest.test_due_date_itself_is_not_overdue) ... ok
test_lists_only_overdue_books_sorted (test_report.OverdueTest.test_lists_only_overdue_books_sorted) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK
$ git add lending.py test_fine.py
$ git commit -m "fix(fine): never charge a negative fine" -m "An early return gave a negative fine since the days_late refactor." -m "Fixes #7"
[fix/negative-fine da9e35e] fix(fine): never charge a negative fine
 2 files changed, 4 insertions(+), 1 deletion(-)
$ git add CHANGELOG.md
$ git commit -m "chore(release): 1.0.1"
[fix/negative-fine 5b3fc4f] chore(release): 1.0.1
 1 file changed, 5 insertions(+)
$ git push -u origin fix/negative-fine
To ../origin.git
 * [new branch]      fix/negative-fine -> fix/negative-fine
branch 'fix/negative-fine' set up to track 'origin/fix/negative-fine'.
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
Already up to date.
$ git merge --no-ff fix/negative-fine -m "Merge pull request #8 from fix/negative-fine"
Merge made by the 'ort' strategy.
 CHANGELOG.md | 5 +++++
 lending.py   | 2 +-
 test_fine.py | 3 +++
 3 files changed, 9 insertions(+), 1 deletion(-)
$ git tag -a v1.0.1 -m "lending 1.0.1"
$ git push --follow-tags
To ../origin.git
   60c8dab..60b78d1  main -> main
 * [new tag]         v1.0.1 -> v1.0.1
$ git branch -d fix/negative-fine
Deleted branch fix/negative-fine (was 5b3fc4f).
$ git push origin --delete fix/negative-fine
To ../origin.git
 - [deleted]         fix/negative-fine

Commit message-এর footer Fixes #7 — অনেক platform merge-এর পরে issue নিজে থেকে বন্ধ করে (platform-ভেদে শব্দ আলাদা)। Version 1.0.1, কারণ API একই, শুধু ভুল আচরণ ঠিক হয়েছে: SemVer-এর PATCH. CHANGELOG-এ নতুন অংশ:

## [1.0.1] - 2026-02-15

### Fixed
- An early return no longer gives a negative fine (#7).

শেষে describe দিয়ে কয়েকটি প্রশ্নের উত্তর — “এখন কোন version?”, “fix branch-এর commit release থেকে কত দূরে ছিল?”, আর হিসাবরক্ষকের জরুরি প্রশ্ন “ভুলটি কোন কোন release-এ ছিল?”:

$ git describe
v1.0.1
$ git describe 5b3fc4f
v1.0.0-2-g5b3fc4f
$ git describe 72cb901
fatal: No tags can describe '72cb9012a7b9866d170d303871dc1682b2b4ccaa'.
Try --always, or create some tags.
$ git describe --contains 72cb901
v1.0.0~2^2~1
$ git tag --contains 72cb901
v1.0.0
v1.0.1

v1.0.0-2-g5b3fc4f মানে “v1.0.0-এর পরে ২টি commit, commit-টি 5b3fc4f”. 72cb901-এর আগে কোনো tag নেই, তাই সাধারণ describe ব্যর্থ; --contains উল্টো দিকে খোঁজে: v1.0.0~2^2~1 = v1.0.0 থেকে first parent ধরে দুই ধাপ (af97afe), তার দ্বিতীয় parent (1f8d3cb), তার এক ধাপ আগে। tag --contains বলছে bug দুটি release-এই আছে, তাই ১২ থেকে ১৫ ফেব্রুয়ারির রসিদগুলো আবার দেখতে হবে।

৯ · ধাপ ১০ — Retrospective: সংখ্যা, শিক্ষা আর ঝুঁকি

v1.0.1-এর পরে পুরো দল — লাইব্রেরিয়ানসহ — এক ঘণ্টা বসলেন। Retrospective-এর নিয়ম (পর্ব ৫, Agile আর Scrum): মানুষ নয়, ব্যবস্থা নিয়ে কথা; অনুভূতির সাথে তথ্য। তথ্যের প্রথম উৎস Git নিজেই:

$ git shortlog -sn HEAD
    15	Rahim
     5	Nasrin
$ git shortlog -sn --no-merges HEAD
    10	Rahim
     3	Nasrin
$ git log --format="%h %an %ad %s" --date=iso-strict --reverse
a323299 Rahim 2026-02-01T10:00:00+06:00 chore: initial project skeleton
4a99fdc Rahim 2026-02-01T10:20:00+06:00 ci: add lint and test workflow
86970bb Rahim 2026-02-02T11:00:00+06:00 feat(borrow): borrow and return books
e311026 Rahim 2026-02-04T09:30:00+06:00 test(borrow): reject borrowing a book that is on loan
63acace Rahim 2026-02-04T10:10:00+06:00 Merge pull request #1 from feature/borrow
cadacb4 Nasrin 2026-02-03T12:00:00+06:00 feat(fine): charge 5 taka per late day
4db12a0 Nasrin 2026-02-04T14:40:00+06:00 feat(fine): return the fine from give_back
609f70f Nasrin 2026-02-05T10:15:00+06:00 Merge pull request #2 from feature/fine
63bd2c0 Rahim 2026-02-09T11:00:00+06:00 feat(report): list overdue loans
1bebbf6 Rahim 2026-02-09T14:30:00+06:00 Merge pull request #3 from feature/overdue
72cb901 Rahim 2026-02-10T10:30:00+06:00 refactor(lending): extract days_late helper
1f8d3cb Rahim 2026-02-10T11:00:00+06:00 test(report): cover the due-date boundary
af97afe Rahim 2026-02-11T09:40:00+06:00 Merge pull request #4 from refactor/days-late
27ca69e Nasrin 2026-02-11T10:00:00+06:00 docs(readme): add a usage example
516d42b Nasrin 2026-02-11T11:00:00+06:00 Merge pull request #5 from docs/usage
1aee4eb Rahim 2026-02-12T10:00:00+06:00 chore(release): 1.0.0
60c8dab Rahim 2026-02-12T10:40:00+06:00 Merge pull request #6 from release/1.0.0
da9e35e Rahim 2026-02-15T10:00:00+06:00 fix(fine): never charge a negative fine
5b3fc4f Rahim 2026-02-15T10:10:00+06:00 chore(release): 1.0.1
60b78d1 Rahim 2026-02-15T10:50:00+06:00 Merge pull request #8 from fix/negative-fine

git shortlog লেখক ধরে commit গোনে। ফাঁদ: যেখানে terminal নেই (script, CI), শুধু git shortlog -sn standard input পড়ে আর কিছুই ছাপে না — এখানে প্রথমবার তা-ই হয়েছিল; তাই HEAD স্পষ্ট লেখা। Rahim-এর ১৫-র পাঁচটিই merge, কারণ তিনি maintainer — সংখ্যাটি মানুষ মাপার নয়, একটি ঝুঁকি দেখায় (নিচে R1).

PR-এর lead time, review-এর অপেক্ষা আর DORA-ধাঁচের সংখ্যা

Commit আর merge-এর সময় ওপরের git log থেকে আসল; PR কখন খোলা হলো আর প্রথম review কখন, Git রাখে না — সেগুলো কাল্পনিক PR page থেকে দেওয়া। Lead time এখানে প্রথম commit থেকে merge, আর review turnaround PR খোলা থেকে প্রথম review. DORA-র চারটি মেট্রিকে (পর্ব ৫৯, Continuous delivery আর DevOps) “deployment”-এর জায়গায় release ধরা হয়েছে, কারণ এটি একটি library:

// Retrospective metrics. Commit and merge times are REAL (git log --date=iso-strict);
// PR "opened" and "first review" times are STATED from the fictional PR page (Git keeps no PRs).
const T = s => Date.parse(s + ":00+06:00");
const PRS = [ // [#, first commit, PR opened, first review, merge commit]
  [1, "2026-02-02T11:00", "2026-02-02T11:05", "2026-02-03T15:00", "2026-02-04T10:10"],
  [2, "2026-02-03T12:00", "2026-02-03T12:05", "2026-02-04T11:00", "2026-02-05T10:15"],
  [3, "2026-02-09T11:00", "2026-02-09T11:05", "2026-02-09T14:00", "2026-02-09T14:30"],
  [4, "2026-02-10T10:30", "2026-02-10T11:05", "2026-02-11T09:30", "2026-02-11T09:40"],
  [5, "2026-02-11T10:00", "2026-02-11T10:05", "2026-02-11T10:50", "2026-02-11T11:00"],
  [6, "2026-02-12T10:00", "2026-02-12T10:05", "2026-02-12T10:30", "2026-02-12T10:40"],
  [8, "2026-02-15T10:00", "2026-02-15T10:15", "2026-02-15T10:40", "2026-02-15T10:50"],
];
const h = ms => (ms / 3.6e6).toFixed(1) + "h";
const median = a => { const s = [...a].sort((x, y) => x - y), m = s.length >> 1;
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
const lead = [], wait = [];
for (const [n, first, open, review, merged] of PRS) {
  lead.push(T(merged) - T(first)); wait.push(T(review) - T(open));
  console.log(`PR #${n}  lead ${h(T(merged) - T(first)).padStart(5)}  review wait ` +
    `${h(T(review) - T(open)).padStart(5)}  open->merge ${h(T(merged) - T(open)).padStart(5)}`);
}
console.log(`median lead ${h(median(lead))} | median review wait ${h(median(wait))}`);
// DORA-style numbers; a release (tag) stands in for a deployment.
const REL = { "v1.0.0": T("2026-02-12T10:45"), "v1.0.1": T("2026-02-15T10:55") };
const CHANGES = [ // author time of every non-merge commit -> first tag that contains it
  ["2026-02-01T10:00", "v1.0.0"], ["2026-02-01T10:20", "v1.0.0"], ["2026-02-02T11:00", "v1.0.0"],
  ["2026-02-04T09:30", "v1.0.0"], ["2026-02-03T12:00", "v1.0.0"], ["2026-02-04T14:40", "v1.0.0"],
  ["2026-02-09T11:00", "v1.0.0"], ["2026-02-10T10:30", "v1.0.0"], ["2026-02-10T11:00", "v1.0.0"],
  ["2026-02-11T10:00", "v1.0.0"], ["2026-02-12T10:00", "v1.0.0"],
  ["2026-02-15T10:00", "v1.0.1"], ["2026-02-15T10:10", "v1.0.1"]];
const lt = CHANGES.map(([t, tag]) => REL[tag] - T(t));
const days = (REL["v1.0.1"] - T("2026-02-01T10:00")) / 864e5;
console.log(`releases 2 in ${days.toFixed(1)} days = ${(2 / days * 7).toFixed(2)} per week`);
console.log(`lead time for changes: median ${h(median(lt))}, max ${h(Math.max(...lt))}`);
console.log(`change failure rate 1/2 = 50% | time to restore ${h(REL["v1.0.1"] - T("2026-02-15T09:00"))}`);

আউটপুট:

PR #1  lead 47.2h  review wait 27.9h  open->merge 47.1h
PR #2  lead 46.3h  review wait 22.9h  open->merge 46.2h
PR #3  lead  3.5h  review wait  2.9h  open->merge  3.4h
PR #4  lead 23.2h  review wait 22.4h  open->merge 22.6h
PR #5  lead  1.0h  review wait  0.8h  open->merge  0.9h
PR #6  lead  0.7h  review wait  0.4h  open->merge  0.6h
PR #8  lead  0.8h  review wait  0.4h  open->merge  0.6h
median lead 3.5h | median review wait 2.9h
releases 2 in 14.0 days = 1.00 per week
lead time for changes: median 71.8h, max 264.8h
change failure rate 1/2 = 50% | time to restore 1.9h

প্রথম দুটি লাইনই প্রধান গল্প: PR #1-এর ৪৭.২ ঘণ্টার ২৭.৯ ঘণ্টা শুধু review-এর অপেক্ষা, PR #2-এ ২২.৯ — কাজ মিনিটের, অপেক্ষা দিনের। PR #4-ও (bug-এর refactor) ২২.৪ ঘণ্টা অপেক্ষার পরে দ্রুত approve হয়েছিল। Median (৩.৫ ঘণ্টা) এই দেরি লুকায়, তাই প্রতিটি সারি দেখুন। Lead time for changes-এর median ৭১.৮ ঘণ্টা, কারণ প্রথম দিনের commit-ও release পর্যন্ত ১১ দিন অপেক্ষা করেছে। Deployment frequency সপ্তাহে ১, change failure rate ২টির ১টি = ৫০%, time to restore ১.৯ ঘণ্টা (০৯:০০ থেকে ১০:৫৫)। সব সময় দেয়াল-ঘড়ির, ছুটিসহ; দুটি release-এর শতাংশ আলোচনার শুরু, রায় নয় (ভাবনার প্রশ্ন ৫)।

Burndown আর velocity

দুটি sprint-এর point আর কোন দিন কোন story-র PR merge হলো (definition of done: merge আর CI সবুজ), তা থেকে burndown আর velocity:

// Burndown and velocity for two 5-day sprints (Sunday-Thursday).
// Story points are STATED planning data. A story counts as done on the day its PR
// was merged (same dates as the commit log): PR #1 on day 4, PR #2 on day 5, PR #3 on day 2.
const SPRINTS = [
  { name: "Sprint 1 (Feb 1-5)",  points: { S1: 3, S2: 2, S3: 1, S4: 5 },
    doneOn: { S1: 4, S2: 4, S3: 4, S4: 5 } },
  { name: "Sprint 2 (Feb 8-12)", points: { S5: 3, S6: 3 },
    doneOn: { S5: 2 } },                     // S6 was not finished
];
const DAYS = 5, velocity = [];
for (const sp of SPRINTS) {
  const total = Object.values(sp.points).reduce((a, b) => a + b, 0);
  console.log(`${sp.name}: committed ${total} points`);
  let left = total;
  for (let d = 1; d <= DAYS; d++) {
    for (const [s, day] of Object.entries(sp.doneOn)) if (day === d) left -= sp.points[s];
    const ideal = total * (1 - d / DAYS);
    console.log(`  day ${d}  left ${String(left).padStart(2)}  ideal ${ideal.toFixed(1).padStart(4)}  ` +
      "#".repeat(left) + (left > ideal ? "   (behind ideal)" : ""));
  }
  const done = total - left;
  velocity.push(done);
  console.log(`  velocity ${done}, commitment kept ${Math.round(100 * done / total)}%`);
}
const mean = velocity.reduce((a, b) => a + b, 0) / velocity.length;
const backlog = 3 + 5;                       // S6 (3) + a new story estimated at 5
console.log(`mean velocity ${mean.toFixed(1)} -> backlog ${backlog} points needs ` +
  `about ${Math.ceil(backlog / mean)} sprint(s); range ${Math.ceil(backlog / Math.max(...velocity))}` +
  `-${Math.ceil(backlog / Math.min(...velocity))} using best and worst sprint`);

আউটপুট:

Sprint 1 (Feb 1-5): committed 11 points
  day 1  left 11  ideal  8.8  ###########   (behind ideal)
  day 2  left 11  ideal  6.6  ###########   (behind ideal)
  day 3  left 11  ideal  4.4  ###########   (behind ideal)
  day 4  left  5  ideal  2.2  #####   (behind ideal)
  day 5  left  0  ideal  0.0
  velocity 11, commitment kept 100%
Sprint 2 (Feb 8-12): committed 6 points
  day 1  left  6  ideal  4.8  ######   (behind ideal)
  day 2  left  3  ideal  3.6  ###
  day 3  left  3  ideal  2.4  ###   (behind ideal)
  day 4  left  3  ideal  1.2  ###   (behind ideal)
  day 5  left  3  ideal  0.0  ###   (behind ideal)
  velocity 3, commitment kept 50%
mean velocity 7.0 -> backlog 8 points needs about 2 sprint(s); range 1-3 using best and worst sprint

Sprint ১-এর রেখা তিন দিন সমতল, তারপর হঠাৎ নামে — পরিচিত “hockey stick”: story “done” হয় merge-এ, আর merge আটকে ছিল review-এ। Sprint ২-এ refactor, README আর release-এর কাজ point-এ ধরা ছিল না — সময় খেয়ে ফেলল, S6 বাদ। Velocity ১১ আর ৩; এত ভিন্ন দুটি সংখ্যার গড় দুর্বল পূর্বাভাস, তাই পরিসর: বাকি ৮ point ১ থেকে ৩ sprint (পর্ব ৬০-এর শিক্ষা)।

কী ভালো গেল, কী খারাপ, পরের বার কী

ভালো গেছেখারাপ গেছেপরের বার (মালিক)
প্রথম দিন থেকে CI; main-এর প্রতিটি commit সবুজ, তাই bisect চারবারে শেষ“আগে ফেরতে ০” কোথাও লেখা ছিল না; কেউ test-ও লেখেননিপ্রতিটি AC-র partition তালিকা review-এর চেকলিস্টে (Nasrin)
Review সত্যিকারের ভুল ধরেছে: S3-এর test, S4-এর AC3Review-এর অপেক্ষা ২২–২৮ ঘণ্টা; বড় অংশ lead timeদিনে দুবার নির্দিষ্ট সময়ে review — সকাল ১০টা আর বিকেল ৩টা (দুজনে)
Conflict-এর সমাধান ছোট, নতুন আচরণ আলাদা commit-এRefactor-এর PR-এ নতুন test শুধু overdue-র সীমা ছুঁয়েছে, fine-এর নয়Refactor-এর আগে mutation testing দিয়ে test-এর জোর দেখা (Rahim)
Bug report থেকে release ১.৯ ঘণ্টায়, regression test-সহRelease-এর কাজ sprint-এ point-হীন, S6 বাদRelease আর refactor-এর কাজও backlog-এ দৃশ্যমান রাখা (লাইব্রেরিয়ান)

Risk register

পর্ব ৬২-এর (Risk আর quality management) মতো প্রতিটি ঝুঁকির সম্ভাবনা (P) আর প্রভাব (I) ১–৫ মাপে, আর exposure = P × I. এই একটি ছোট risk register:

IDঝুঁকিPIP×Iপ্রতিকার
R1একজনই maintainer — Rahim না থাকলে release আর tag আটকে যায়৩৪১২Nasrin-কে maintainer অনুমতি; release-এর ধাপ README-তে
R2লেখা-না-থাকা নিয়ম (যেমন আগে ফেরত) আবার বাদ পড়া৪৩১২AC-তে partition-এর চেকলিস্ট; লাইব্রেরিয়ানের সাথে উদাহরণ ধরে AC যাচাই
R3Memory-তে হিসাব — বিদ্যুৎ চলে গেলে বা program বন্ধ হলে দিনের ধার হারানো (ADR 0001-এর জানা ফল)৩৪১২v1.1-এ একটি storage port আর file বা SQLite adapter (পর্ব ৪০-এর hexagonal ধারণা)
R4Review-এর দীর্ঘ অপেক্ষায় বড় PR আর তাড়াহুড়োর review৩৩৯নির্দিষ্ট review-এর সময়; PR ছোট রাখা
R5কমিটি জরিমানার হার বদলালে পুরোনো ধারের হিসাব২৩৬হারের নিয়ম লিখে নেওয়া: কোন তারিখ থেকে কোন হার — নতুন story

নিজে করুন — নিজের ধারণা নিয়ে একই পথ

এবার আপনার পালা: একটি ছোট ধারণা — বাজারের ফর্দ, ক্লাবের চাঁদা, ওষুধের সময়সূচি — দুই সপ্তাহে দুটি release. একা হলে দুটি clone-এ দুজনের ভূমিকা নিন, বা একজন বন্ধুকে reviewer করুন:

নিজেকে মূল্যায়নের rubric

এই rubric-এ নিজের project-এর প্রতিটি সারি সৎভাবে বসান — “শুরু” লজ্জার নয়, পরের লক্ষ্য:

ধাপশুরুভালোGold Standard
Requirementবৈশিষ্ট্যের তালিকাUser story আর ACGiven-When-Then, partition আর সীমা, মাপা যায় এমন NFR, traceability
Plan“দুই সপ্তাহে হবে”Story point আর sprintপরিসরসহ পূর্বাভাস, critical path, অপেক্ষার সময়ও পরিকল্পনায়
Designসরাসরি কোডClass diagramDomain-এর ভাষায় নাম, নীতির যুক্তি, বিকল্পসহ ADR
Repositoryশুধু কোডREADME, .gitignoreCONTRIBUTING, প্রথম দিনেই CI, branch protection, Conventional Commits
Testহাতে চালিয়ে দেখাUnit test, CI-তেTest আগে, প্রতিটি AC-র partition, mutation দিয়ে test-এর জোর যাচাই
দলগত কাজসবাই main-এPR আর approvalছোট PR, লেবেলসহ সদয় মন্তব্য, conflict-এর সমাধান আলাদা commit থেকে পৃথক, দ্রুত review
Release“নতুন zip”TagAnnotated tag, মানুষের জন্য CHANGELOG, SemVer-এর যুক্তি
Bug-এর পরেHotfix, test ছাড়াFix আর testRegression test আগে, bisect-এ দোষী commit, কোন release-এ ছিল তার তালিকা, patch release
RetrospectiveনেইআলোচনাGit থেকে সংখ্যা, মালিকসহ action item, হালনাগাদ risk register

পরীক্ষা ও ইন্টারভিউতে। প্রশ্ন: একটি project-এর শুরু থেকে release পর্যন্ত Git workflow বর্ণনা করুন। উত্তর: protected main, ছোট feature branch, PR-এ CI আর review, merge-এর আগে rebase, annotated tag আর CHANGELOG. প্রশ্ন: Release-এর পরে একটি regression পেলে কী করবেন? উত্তর: পুনরুৎপাদন, regression test, git bisect run, test-সহ fix, patch version. প্রশ্ন: Rebase-এর conflict-এ HEAD কোনটি? উত্তর: যে base-এর ওপর বসানো হচ্ছে, আর ইতিমধ্যে replay হওয়া commit — আপনার নিজের commit নয়। প্রশ্ন: ১০০% line coverage থাকলেও bug কীভাবে? উত্তর: coverage লাইন চলা মাপে, আচরণের partition নয়; mutation testing দুর্বলতা দেখায়। প্রশ্ন: Change failure rate কীভাবে হিসাব করেন? উত্তর: যে release বা deployment-এর পরে সমাধান (hotfix, rollback) লেগেছে, তাদের সংখ্যা ÷ মোট।

আরও গভীরে। এই project-এর artefact-গুলোর আনুষ্ঠানিক রূপ আছে: requirement-এর জন্য ISO/IEC/IEEE 29148, quality-র জন্য ISO/IEC 25010, আর software-এর জীবনচক্রের process-এর জন্য ISO/IEC/IEEE 12207. Retrospective-এর কাঠামো নিয়ে Esther Derby ও Diana Larsen-এর Agile Retrospectives (২০০৬) পরিচিত বই; DORA-র মেট্রিকের পটভূমি Forsgren, Humble ও Kim-এর Accelerate (২০১৮)।

১০ · AI-এর যুগে — capstone-এ AI-কে সহকারী রাখা, কর্তা নয়

প্রতিটি ধাপে AI সাহায্য করতে পারে — story-র খসড়া, test-এর ধারণা, YAML, commit message. কিন্তু প্রতিটি ধাপে তার চেনা ভুলও আছে, আর capstone-এ সেগুলো জমে:

AI-এর চেনা ভুলকীভাবে ধরবেন
AC লেখে শুধু “সুখের পথ”-এ; আগে ফেরতের মতো partition বাদপ্রতিটি AC-র জন্য নিজে partition আর সীমার তালিকা; AI-কে আলাদা করে “কোন partition বাদ” জিজ্ঞেস করা
Test কোডকে নকল করে: assertEqual(fine(d, r), max(0, ...) * 5) — একই সূত্র দুবার, কিছুই যাচাই হয় নাTest-এ হাতে হিসাব করা সংখ্যা (০, ১৫, ৩০); mutation testing
Conflict “সমাধান”-এ এক পক্ষের কোড চুপচাপ ফেলে দেয়, বা git checkout --theirs-এর পরামর্শ দেয় rebase-এর উল্টো অর্থ না বুঝেসমাধানের পরে git diff আর পুরো test; rebase-এ ours/theirs-এর অর্থ মনে রাখা
“Push rejected”-এ git push --force, এমনকি main-এ, সতর্কতা ছাড়াশুধু নিজের branch-এ --force-with-lease; main protected
অস্তিত্বহীন flag বা action-এর input, পুরোনো versiongit help <command>, action-এর action.yml; throwaway clone-এ আগে চালানো
ছোট project-এ pattern-এর স্তূপ (factory, strategy…), দেখতে ঠিক কিন্তু ভুল UML (উল্টো diamond, বানানো multiplicity), বা অস্তিত্বহীন standard-এর নামপ্রতিটি pattern-এর জন্য “কোন সমস্যা মেটায়?”; UML-এর notation পর্ব ২৯-এ মিলিয়ে; standard-এর নাম ও সাল উৎসে
Prompt-এ পুরো repository, .env বা সদস্যদের ব্যক্তিগত তথ্য পাঠানোশুধু দরকারি অংশ; secret আর ব্যক্তিগত তথ্য বাদ, কাল্পনিক id

নিজের capstone-এর একটি PR-এর review-এ AI-কে দ্বিতীয় চোখ হিসেবে ব্যবহারের prompt:

তুমি একজন সতর্ক senior reviewer. নিচে একটি কাল্পনিক project-এর user story, তার acceptance criteria, আর একটি PR-এর diff দিলাম (কোনো secret বা ব্যক্তিগত তথ্য নেই)। [story আর Given-When-Then] [git diff main...feature/x] ১. প্রতিটি acceptance criterion-এর জন্য বলো কোন test সেটি যাচাই করে; যেটির test নেই, আলাদা তালিকায় দাও। ২. প্রতিটি input-এর equivalence partition আর সীমা লেখো, আর কোনটির test নেই বলো। ৩. এমন একটি ছোট mutation (যেমন একটি max বা তুলনা চিহ্ন বদল) প্রস্তাব করো, যা বর্তমান test পার হয়ে যাবে। ৪. মন্তব্য Conventional Comments-এর লেবেলে দাও (issue, suggestion, question, praise), blocking কি না বলে। ৫. কোনো Git command প্রস্তাব করলে বলো সেটি history বদলায় কি না; force push বা reset --hard প্রস্তাব করবে না। ৬. নিশ্চিত না হলে "নিশ্চিত নই" লেখো; কোনো version, flag বা মান বানাবে না।

AI-সহায়তায় capstone — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১জরিমানার bug-টি requirement, test, code review আর CI — চারটি স্তর পেরিয়ে release-এ গেল। এত স্তর থাকতেও এমন হয় কেন, আর কোন একটি অনুশীলন এটি সবচেয়ে আগে ধরত?

নিরাপত্তা-বিজ্ঞানে এর একটি পরিচিত ছবি আছে: মনোবিজ্ঞানী James Reason-এর “Swiss cheese model” (তাঁর Human Error, ১৯৯০, আর পরের লেখায় জনপ্রিয়)। প্রতিটি প্রতিরক্ষার স্তর একটি পনিরের টুকরো, যার গায়ে ফুটো আছে; দুর্ঘটনা ঘটে যখন সব টুকরোর ফুটো এক সারিতে আসে। এই project-এ ফুটোগুলো ঠিক এক সারিতে ছিল, আর প্রতিটির কারণ আলাদা।

Requirement-এর ফুটো: “আগে ফেরতে ০” এত স্বাভাবিক যে কেউ লেখেননি। Test-এর ফুটো: test লেখা হয়েছিল লেখা AC থেকে, তাই অলিখিত partition-এর test নেই। Coverage-এর ফুটো: ১০০% line coverage আশ্বাস দিয়েছে, অথচ সেটি আচরণের মাপ নয়। Review-এর ফুটো: PR #4-এর শিরোনাম “refactor”, আর refactor মানে “আচরণ বদলায় না” — reviewer সেই প্রত্যাশা নিয়ে diff পড়েছেন, আর ২২ ঘণ্টা অপেক্ষার পরে দ্রুত approve করেছেন। CI-এর ফুটো: CI ততটুকুই জানে, যতটুকু test জানে।

কোন অনুশীলন সবচেয়ে আগে ধরত? সম্ভবত mutation testing (পর্ব ৫০)। Refactor-এর আগে v1-এর fine()-এ max(0, late)-কে late-এ বদলানো একটি সাধারণ mutation; পুরোনো দুটি test সেই mutant-কে মারতে পারত না — অর্থাৎ ঠিক যে পরিবর্তনটি পরে ভুল করে ঘটল, তা আগেই “বেঁচে থাকা mutant” হিসেবে দেখা যেত। আরেকটি সস্তা পথ ছিল AC-র partition-এর চেকলিস্ট: প্রতিটি সংখ্যাভিত্তিক নিয়মে “কম, সমান, বেশি” — তিনটিরই উদাহরণ।

লক্ষ করুন, কোনো একটি স্তরকে নিখুঁত করা লক্ষ্য নয়; লক্ষ্য হলো স্তরগুলোর ফুটো যেন একই কারণে না হয়। এখানে সব ফুটোর মূল ছিল একটি — অলিখিত requirement — তাই একটি ভুল চারটি স্তর একসাথে পার হলো।

মূল উপলব্ধি: স্তর গুনে নিরাপত্তা হয় না; স্তরগুলো একে অন্যের থেকে আলাদা উৎস থেকে তথ্য নিলে হয়। যে কথা লেখা নেই, তা কোনো স্তরই দেখে না — তাই partition লেখা আর mutation দিয়ে test-এর জোর যাচাই সবচেয়ে সস্তা প্রতিরক্ষা।

প্র ০২একটি “নিরীহ” refactor একটি নীরব অনুমান হারিয়ে ফেলল। ইতিহাসের কোন বিখ্যাত ব্যর্থতা একই ধাঁচের, আর তা থেকে refactoring ও পুনর্ব্যবহারে কী শেখা যায়?

সবচেয়ে পরিচিত উদাহরণ Ariane 5-এর প্রথম উড্ডয়ন, Flight 501, ৪ জুন ১৯৯৬. উৎক্ষেপণের প্রায় ৩৭ সেকেন্ড পরে রকেটটি পথ হারিয়ে ভেঙে পড়ে। Jacques-Louis Lions-এর নেতৃত্বে তদন্ত বোর্ডের প্রকাশিত প্রতিবেদন অনুযায়ী, inertial reference system-এর software Ariane 4 থেকে প্রায় অপরিবর্তিত নেওয়া হয়েছিল। তার ভেতরে একটি 64-bit floating-point মানকে 16-bit signed integer-এ রূপান্তর ছিল, যার সুরক্ষা-পরীক্ষা কিছু variable-এর জন্য ইচ্ছে করে বাদ রাখা হয়েছিল — কারণ Ariane 4-এর উড্ডয়ন-পথে সেই মান কখনো এত বড় হতো না। Ariane 5 দ্রুততর, মানটি সীমা ছাড়াল, overflow-এর ব্যতিক্রমে প্রধান আর বিকল্প দুটি যন্ত্রই একই কারণে বন্ধ হলো। প্রতিবেদনে আরও বলা হয়, যে অংশে ভুলটি ঘটে, উৎক্ষেপণের পরে সেটির কোনো কাজই ছিল না।

আমাদের ছোট bug-এর সাথে মিল গঠনগত। কোডটি একটি প্রসঙ্গে সঠিক ছিল, কারণ একটি অনুমান সত্য ছিল (Ariane 4-এর পথ; fine()-এর ভেতরে max). কোড নতুন প্রসঙ্গে গেল (নতুন রকেট; নতুন helper), আর অনুমানটি কোথাও লেখা না থাকায় সঙ্গে গেল না। দুই ক্ষেত্রেই test ছিল, কিন্তু নতুন প্রসঙ্গের partition নিয়ে নয়।

শিক্ষা তিনটি। প্রথম, অনুমানকে কোডে দৃশ্যমান করুন — assertion, type, বা অন্তত একটি test, যা বলে “এই মান কখনো ঋণাত্মক নয়” (পর্ব ৩১-এর design by contract). দ্বিতীয়, পুনর্ব্যবহার বা refactor-এর সময় পুরোনো কোডের “কেন” পড়ুন, শুধু “কী” নয় — max(0, …) কেন ছিল, সেই প্রশ্নই bug আটকাত। তৃতীয়, “অপ্রয়োজনীয় কিন্তু চলছে” এমন কোডও ঝুঁকি; Ariane-এর ক্ষেত্রে সেটিই ব্যর্থ হয়েছিল।

তুলনাটি অনুপাতে নয় — একটি পাঠাগারের −২০ টাকা আর একটি রকেট এক নয়। কিন্তু ব্যর্থতার ধরন একই, আর বড় ব্যর্থতার ধরন ছোট project-এ চেনা শেখাই সবচেয়ে সস্তা প্রশিক্ষণ।

মূল উপলব্ধি: কোড তার প্রসঙ্গের অনুমান বহন করে, কিন্তু অনুমানগুলো লেখা না থাকলে নতুন প্রসঙ্গে হারায়। Refactor আর পুনর্ব্যবহারের আগে অনুমানগুলোকে test বা assertion-এ রূপ দিন।

প্র ০৩এই দল merge-এর আগে rebase করে, আবার merge করে --no-ff দিয়ে। Rebase-পন্থী আর merge-পন্থী — দুই দলের যুক্তি কী, আর মাঝের এই পথটি কেন বেছে নেওয়া?

বিতর্কটি Git-এর প্রায় জন্ম থেকে। Merge-পন্থীরা বলেন, ইতিহাস হলো যা সত্যিই ঘটেছিল — কে কোন base থেকে কাজ শুরু করেছিলেন, কখন মেলালেন। Rebase সেই সত্যকে নতুন করে লেখে; নতুন commit-গুলো এমন code অবস্থায় “তৈরি” দেখায়, যেখানে লেখক আসলে কখনো test চালাননি। Linux kernel-এর আলোচনায় Linus Torvalds বহুবার সতর্ক করেছেন অন্যের সাথে ভাগ করা (public) ইতিহাস rebase না করতে, আর অর্থহীন “sync” merge-ও না করতে — এই দুই নির্দেশনা প্রায়ই একসাথে উদ্ধৃত হয় (তাঁর mailing list-এর লেখা মূল উৎস; শব্দ উৎসভেদে আলাদা)। Rebase-পন্থীরা বলেন, পাঠকের কাছে ইতিহাস একটি গল্প; জট-পাকানো graph-এ bisect, blame আর review কঠিন হয়।

এই দলের পথ দুটির সুবিধা নেয়, ঝুঁকি এড়ায়। Rebase হয় শুধু নিজের, এখনো merge-না-হওয়া feature branch-এ — সেটি কার্যত ব্যক্তিগত, আর --force-with-lease অন্যের কাজ মুছে ফেলার ঝুঁকি কমায়। Rebase-এর পরে test আবার চালানো হয় (Nasrin চালিয়েছিলেন), তাই “কখনো test-না-হওয়া অবস্থা”-র আপত্তিও কমে, আর CI নতুন push-এ আবার চলে। তারপর --no-ff একটি merge commit রাখে, যা PR-এর রেকর্ড: git log --first-parent হলো PR-এর তালিকা, আর একটি পুরো feature এক commit-এ revert করা যায় (git revert -m 1).

বিকল্পগুলোর দামও জানা। Squash merge সবচেয়ে সরল main দেয়, কিন্তু PR-এর ভেতরের ছোট ধাপগুলো হারায় — আমাদের bisect 72cb901 আর 1f8d3cb-কে আলাদা করতে পারত না, শুধু পুরো PR #4 দেখাত। শুধু merge (rebase ছাড়া) conflict-এর সমাধানকে merge commit-এর ভেতরে লুকায়, যা review করা কঠিন। আর trunk-based দলে (পর্ব ১৭) branch এত ছোট যে প্রশ্নটিই প্রায় ওঠে না।

মূল উপলব্ধি: ভাগ করা ইতিহাস নয়, শুধু নিজের অপ্রকাশিত কাজ rebase করুন; আর merge-এর ধরন বাছুন পরে কোন প্রশ্নের উত্তর সহজ চান তা দেখে — PR-এর তালিকা, ছোট ধাপে bisect, নাকি সবচেয়ে সরল main.

প্র ০৪দুজনের একটি ছোট project-এ PR, review, CI, ADR, CHANGELOG, risk register — এত process কি বাড়াবাড়ি? Agile Manifesto তো “process-এর চেয়ে মানুষ” বলে।

Agile Manifesto (২০০১)-এর প্রথম মূল্যবোধ সত্যিই “individuals and interactions over processes and tools”, কিন্তু তার শেষ বাক্যটি প্রায়ই বাদ পড়ে: ডানের জিনিসগুলোরও মূল্য আছে, শুধু বাঁয়েরগুলোকে বেশি মূল্য দেওয়া হয়। প্রশ্নটি তাই “process থাকবে কি না” নয়, “কোন process তার খরচের চেয়ে বেশি দেয়”।

এই project-এর প্রতিটি অনুশীলনের খরচ আর ফল মাপা যায়। CI সেট করতে লেগেছে একটি commit; তার ফল — প্রতিটি commit সবুজ — bisect-কে চারবারে শেষ করেছে। ADR লিখতে দশ মিনিট; ছয় মাস পরে “database নেই কেন” প্রশ্নের উত্তর। Review-এর খরচ ছিল সবচেয়ে বেশি (দিনের পর দিন অপেক্ষা), কিন্তু দুটি আসল ভুলও ধরেছে; সমাধান review বাদ দেওয়া নয়, তার সময় বদলানো। এর একটি পুরোনো মাপকাঠি আছে: Joel Spolsky ২০০০ সালে “The Joel Test” নামে বারোটি হ্যাঁ-না প্রশ্ন প্রকাশ করেন — source control আছে কি না, এক ধাপে build হয় কি না, bug-এর তালিকা আছে কি না, এমন। প্রশ্নগুলো এত সস্তা আর এত প্রাথমিক যে ছোট দলের জন্যই লেখা।

অন্যদিকে, ছোট দলের ক্ষেত্রে কিছু জিনিস সত্যিই বাদ দেওয়া যায়: দুজনের জন্য আলাদা release branch, বিস্তারিত Gantt chart, বা প্রতিদিনের আনুষ্ঠানিক standup হয়তো লাগে না। মাপকাঠি হলো ঝুঁকি। এই project-এ টাকার হিসাব আছে (জরিমানা), তাই test আর review-এর মূল্য বেশি। আর “bus factor” — একজন মানুষ হারালে project কতটা আটকায় — দুজনের দলে সবচেয়ে বড় ঝুঁকি, যা risk register-এর R1-এ উঠেছে।

আর একটি কারণ শিক্ষামূলক: capstone-এর উদ্দেশ্যই হলো অভ্যাস তৈরি। দুজনের project-এ যে অনুশীলন সহজ, পঞ্চাশ জনের দলে সেটি বাঁচার শর্ত; সেখানে প্রথমবার শেখা অনেক দামি।

মূল উপলব্ধি: Process-এর মাপ খরচ বনাম কমানো ঝুঁকি। যে অনুশীলন সস্তা আর পরে বড় প্রশ্নের উত্তর দেয় — version control, CI, test, লেখা সিদ্ধান্ত — তা ছোট দলেও রাখুন; আনুষ্ঠানিকতা বাড়ান ঝুঁকি বাড়লে।

প্র ০৫দুটি release-এ change failure rate ৫০% — এই সংখ্যা কতটা অর্থবহ? DORA-র মেট্রিক ছোট দলে কীভাবে ব্যবহার করা উচিত, আর কীভাবে নয়?

DORA-র চারটি মেট্রিক এসেছে বহু বছরের জরিপভিত্তিক গবেষণা থেকে, যার সারাংশ Nicole Forsgren, Jez Humble ও Gene Kim-এর Accelerate (২০১৮)। সেখানে হাজার হাজার উত্তরদাতার তথ্যে দেখা গিয়েছিল, deployment-এর ঘনত্ব, lead time, ব্যর্থতার হার আর পুনরুদ্ধারের সময় একসাথে ভালো দলগুলোকে আলাদা করে। এগুলো দল-স্তরের প্রবণতার মাপ, অনেক deployment-এর ওপর গড়, আর গবেষণার মডেলে প্রতিষ্ঠানের ফলের সাথে সম্পর্কিত — একটি project-এর দুটি release-এর ওপর রায় দেওয়ার যন্ত্র নয়। পরের বছরগুলোর DORA প্রতিবেদনে মেট্রিকের নাম ও সংজ্ঞায় কিছু বদলও এসেছে বলে জানা যায়, তাই সংজ্ঞা লেখার সময়ের উৎসে মিলিয়ে নিন।

দুটি release-এ ৫০% মানে শুধু “একটিতে সমস্যা হয়েছিল”। পরের release ঠিক গেলে ৩৩%, তারপর ২৫% — সংখ্যা লাফাবে, অথচ দল একই। ছোট নমুনায় তাই শতাংশের চেয়ে ঘটনাটি নিজে বেশি তথ্য দেয়: কোন ভুল, কোন স্তর পেরোল, কত দ্রুত ফেরা গেল। আর আমাদের সব সময় দেয়াল-ঘড়ির — শুক্র-শনির ছুটি ধরা — কিন্তু তুলনা করলে একই নিয়মে মাপতে হবে।

আরেকটি পুরোনো বিপদ মেট্রিককে লক্ষ্য বানানো। অর্থনীতিবিদ Charles Goodhart-এর ১৯৭৫ সালের পর্যবেক্ষণ থেকে আসা “Goodhart's law”-এর প্রচলিত রূপ: কোনো মাপ লক্ষ্য হয়ে গেলে ভালো মাপ থাকে না। Release-এর সংখ্যা বাড়াতে বললে দল অর্থহীন ছোট release দেবে; ব্যর্থতার হার কমাতে বললে bug-কে “ব্যর্থতা” বলা বন্ধ হবে। git shortlog-এর সংখ্যা দিয়ে মানুষ মাপলে commit ভাঙা শুরু হবে।

ভালো ব্যবহার তাই আয়নার মতো: প্রবণতা দেখা, প্রশ্ন তোলা (“review-এর অপেক্ষা কেন ২৮ ঘণ্টা?”), আর একটি পরিবর্তনের আগে-পরে তুলনা। এই দলের জন্য সবচেয়ে কাজের সংখ্যা ছিল DORA-র বাইরে — review-এর অপেক্ষা — যা সরাসরি একটি action item দিল।

মূল উপলব্ধি: মেট্রিক প্রশ্ন তোলে, উত্তর দেয় না। ছোট নমুনায় ঘটনাটিকে পড়ুন, শতাংশকে নয়; আর কোনো সংখ্যাকে কারও লক্ষ্য বা মূল্যায়ন বানাবেন না।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. বর্ধিত কাজ — একটি নতুন feature শুরু থেকে শেষ: backlog-এর S6 (“একজন সদস্য একসাথে ৩টির বেশি বই নয়”) v1.0.1-এর ওপর পুরো পথে করুন: (ক) Given-When-Then-এ AC, partition-সহ; (খ) story point আর কেন; (গ) branch-এর নাম আর প্রথম লাল test; (ঘ) ন্যূনতম implementation; (ঙ) commit message; (চ) নতুন version নম্বর আর তার যুক্তি।

    (ক) AC: Given M1-এর কাছে B1, B2, B3, When M1 B4 চান, Then অনুরোধ বাতিল (ValueError) আর তালিকা অপরিবর্তিত। Given M1-এর কাছে ৩টি, When M2 B4 চান, Then M2 পান (অন্য সদস্য প্রভাবিত নন)। Given M1-এর কাছে ৩টি, When একটি ফেরত দিয়ে আবার চান, Then পান। Partition: ধরে রাখা বই ০–২ (অনুমতি), ঠিক ৩ (সীমা, নিষেধ), আর সীমা না দেওয়া Library (আগের মতো সীমাহীন)।

    (খ) ৩ point — S1-এর সমান জটিলতা, একটি গণনা আর একটি শর্ত; নতুন partition আছে বলে ২ নয়।

    (গ–ঘ) git switch -c feature/member-limit, তারপর test_borrow.py-তে আগে এই test (লাল: Library() max_books নেয় না), তারপর implementation. দুটি একসাথে v1.0.1-এর ওপর চালিয়ে দেখা হয়েছে — ১০টি test সবুজ:

        def test_member_cannot_hold_more_than_the_limit(self):
            lib = Library(max_books=3)
            for book in ("B1", "B2", "B3"):
                lib.borrow(book, "M1", DAY)
            with self.assertRaises(ValueError):
                lib.borrow("B4", "M1", DAY)
            lib.borrow("B4", "M2", DAY)  # another member is not affected
            self.assertEqual(lib.loans["B4"][0], "M2")
    
    # lending.py
        def __init__(self, max_books=None):
            self.loans = {}  # book -> (member, borrowed_on)
            self.max_books = max_books  # None = no limit (v1.0 behaviour)
    
        def borrow(self, book, member, day):
            if book in self.loans:
                raise ValueError(f"{book} is already borrowed")
            held = sum(1 for m, _ in self.loans.values() if m == member)
            if self.max_books is not None and held >= self.max_books:
                raise ValueError(f"{member} already holds {held} books")
            self.loans[book] = (member, day)

    (ঙ) feat(borrow): limit books per member, body-তে “Story S6. Library(max_books=3) rejects a fourth loan; default None keeps the old behaviour.”

    (চ) 1.1.0 — কারণ সীমা ঐচ্ছিক, ডিফল্টে আগের আচরণ; পুরোনো সব ডাক আগের মতোই চলে, তাই নতুন সুবিধা = MINOR. যদি সীমা সবসময় ৩ করা হতো (ডিফল্ট ৩), তাহলে আগে সফল চতুর্থ ধার এখন ব্যর্থ — প্রকাশ্য আচরণের ভাঙা পরিবর্তন, SemVer-এ কঠোরভাবে 2.0.0. এই কারণেই নকশায় সীমাটি ঐচ্ছিক রাখা।

  2. আউটপুট অনুমান করুন: একটি ফাঁকা repository-তে (main) এই ক্রম চালানো হলো: commit “chore: skeleton”; git switch -c feature/x; commit “feat: x1”, তারপর “feat: x2”; git switch main; commit “docs: readme”; git switch feature/x; git rebase main; git switch main; git merge --no-ff feature/x -m "Merge feature/x"; git tag -a v0.1.0 -m "0.1.0"; commit “fix: typo”. (ক) git log --graph --format="%s%d" --all কী ছাপবে? (খ) git describe-এর আকার কী (hash ছাড়া)? (গ) git log --first-parent --format=%s? (ঘ) git rev-list --count HEAD?

    আসলে চালালে (Git ২.৫৩, নির্দিষ্ট তারিখ দিয়ে):

    $ git log --graph --format="%s%d" --all
    * fix: typo (HEAD -> main)
    *   Merge feature/x (tag: v0.1.0)
    |\
    | * feat: x2 (feature/x)
    | * feat: x1
    |/
    * docs: readme
    * chore: skeleton
    $ git describe
    v0.1.0-1-g113e4c3
    $ git log --first-parent --format=%s
    fix: typo
    Merge feature/x
    docs: readme
    chore: skeleton
    $ git rev-list --count HEAD
    6

    Rebase-এর পরে x1 আর x2 “docs: readme”-এর ওপর বসেছে, তাই বুদবুদের নিচের প্রান্ত readme-তে, skeleton-এ নয়। --no-ff fast-forward সম্ভব থাকা সত্ত্বেও merge commit বানিয়েছে। %d ref-এর নাম দেখায়: tag merge-এ, HEAD -> main সবচেয়ে ওপরে। Describe: tag-এর পরে ১টি commit, তাই v0.1.0-1-g<hash> (hash-টি আপনার তারিখে আলাদা হবে)। First-parent পথ x1, x2 এড়িয়ে যায়। মোট commit ৬: skeleton, readme, x1, x2, merge, typo.

  3. সমালোচনা — একটি ত্রুটিপূর্ণ project log: একটি কাল্পনিক “বাজার-ফর্দ” app-এর দলের ইতিহাস ও অভ্যাস নিচে। অন্তত আটটি সমস্যা আর প্রতিটির বিকল্প বলুন।
    9f1c2aa (HEAD -> main, tag: v1) final final
    77b03de fix
    a1d9e40 Merge branch 'main' of https://github.com/example/bazar
    c3e5f11 wip
    5d2a9b0 skip flaky tests for now
    e0b7c66 added stuff + api key for testing
    b4f1a02 Merge branch 'main' of https://github.com/example/bazar
    0a8e113 init
    -- notes: everyone pushes to main; CI runs nightly only;
       main was red for 3 days last week; v1 tagged with "git tag v1";
       no CHANGELOG; requirements live in one chat thread;
       a force push to main "fixed" a bad merge on Tuesday.
    • Secret commit: e0b7c66-এ api key — ফাঁস ধরে নিয়ে key বদলান (rotate), ইতিহাস পরিষ্কার, secret scanning আর .env .gitignore-এ (পর্ব ৫৭)। এটি সবার আগে।
    • অর্থহীন message: “final final”, “fix”, “wip”, “added stuff” — Conventional Commits, কী আর কেন (পর্ব ১৮)।
    • সবাই main-এ push, force push-ও: branch protection, PR আর approval; main-এ force push বন্ধ; ভুল merge ঠিক করতে git revert -m 1 (পর্ব ১১, ১৬)।
    • “Merge branch 'main' of …”: git pull-এর অপ্রয়োজনীয় merge — pull.ff only বা নিজের branch-এ rebase (পর্ব ১৪–১৫)।
    • Test বন্ধ করা: “skip flaky tests” — flaky test মালিক আর সময়সীমাসহ quarantine, কারণ খুঁজে ঠিক করা (পর্ব ৫১)।
    • শুধু রাতের CI আর তিন দিন লাল main: প্রতিটি push আর PR-এ CI, required check, লাল হলে আগে revert (পর্ব ৫৮)।
    • Lightweight tag “v1”: annotated v1.0.0, SemVer-এর তিন অংশ; সাথে CHANGELOG (পর্ব ১৮)।
    • Chat-এ requirement: user story আর AC লেখা, traceability, সিদ্ধান্ত ADR-এ (পর্ব ২২, ২৪, ৩৮)।
    • WIP commit main-এ: bisect-এ ভাঙা commit — PR-এর আগে squash বা গুছিয়ে নেওয়া; main-এর প্রতিটি commit সবুজ।

    অগ্রাধিকার: আগে নিরাপত্তা (key), তারপর মিথ্যা সংকেত বন্ধ (skip করা test, রাতের CI), তারপর প্রবাহ (protection, PR), শেষে পরিচ্ছন্নতা (message, tag, CHANGELOG).

  4. হিসাব (মেট্রিক): আরেকটি কাল্পনিক দলের চার সপ্তাহের তথ্য: ৬টি release, যার ২টির পরে hotfix লেগেছিল; দুটি ঘটনায় পুনরুদ্ধারে লেগেছিল ৩ আর ৫ ঘণ্টা। পাঁচটি PR-এর lead time ৪, ৩০, ৬, ৫২, ৮ ঘণ্টা, আর review-এর অপেক্ষা ১, ২০, ২, ২৬, ৩ ঘণ্টা। তিনটি sprint-এর velocity ১৮, ২২, ১৪; backlog-এ ৪৫ point. বের করুন: (ক) সপ্তাহে release; (খ) change failure rate; (গ) গড় time to restore; (ঘ) lead time-এর median আর গড়, কোনটি বেশি তথ্য দেয়; (ঙ) review-এর অপেক্ষা lead time-এর কত অংশ (মোট); (চ) গড় velocity-তে backlog কত sprint, আর সেরা-খারাপ sprint ধরে পরিসর।

    (ক) ৬ ÷ ৪ = সপ্তাহে ১.৫টি. (খ) ২ ÷ ৬ = ৩৩.৩%. (গ) (৩ + ৫) ÷ ২ = ৪ ঘণ্টা.

    (ঘ) সাজালে ৪, ৬, ৮, ৩০, ৫২ — median ৮ ঘণ্টা; গড় (৪ + ৩০ + ৬ + ৫২ + ৮) ÷ ৫ = ১০০ ÷ ৫ = ২০ ঘণ্টা. গড় দুটি দীর্ঘ PR-এর টানে median-এর আড়াই গুণ; “সাধারণ PR”-এর জন্য median, কিন্তু সমস্যা খুঁজতে দীর্ঘ দুটি নিজেই দেখুন — দুটিরই review-এর অপেক্ষা ২০ আর ২৬ ঘণ্টা।

    (ঙ) Review-এর অপেক্ষা মোট ১ + ২০ + ২ + ২৬ + ৩ = ৫২ ঘণ্টা, lead time মোট ১০০ — ৫২%. শুধু দুটি দীর্ঘ PR-এ (২০ + ২৬) ÷ (৩০ + ৫২) = ৪৬ ÷ ৮২ ≈ ৫৬%.

    (চ) গড় velocity (১৮ + ২২ + ১৪) ÷ ৩ = ১৮; ৪৫ ÷ ১৮ = ২.৫ → প্রায় ৩ sprint. পরিসর: ⌈৪৫ ÷ ২২⌉ = ৩ থেকে ⌈৪৫ ÷ ১৪⌉ = ৪ — ৩–৪ sprint. (হিসাবগুলো একটি ছোট script চালিয়ে মিলিয়েছি।)

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি ছোট software project-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 👥 Stakeholder, user story আর Given-When-Then
  2. 🃏 Story point আর critical path
  3. 📐 Domain model, class diagram আর ADR
  4. 📁 Repository, CI আর branch protection
  5. 🧪 TDD-তে feature, PR review আর merge
  6. 🏷️ CHANGELOG আর annotated tag দিয়ে release

Nasrin তাঁর feature/fine branch-এ git rebase main চালালেন, আর lending.py-তে conflict হলো। File-এ <<<<<<< HEAD থেকে ======= পর্যন্ত অংশে কার লেখা থাকে?

  • Nasrin-এর replay হতে থাকা commit-এর লেখা
  • যে base-এর ওপর বসানো হচ্ছে (main) আর ইতিমধ্যে replay হওয়া commit-এর লেখা
  • দুই branch-এর merge base-এর লেখা
  • Server-এ থাকা origin/feature/fine-এর পুরোনো লেখা

git bisect run ./check.sh-এ একটি পুরোনো commit-এ project build-ই হয় না, তাই সেটি ভালো না খারাপ বলা যায় না। Script কোন exit code দিলে Git commit-টি বাদ দিয়ে (skip) পাশের commit পরীক্ষা করবে?

  • 0
  • 1
  • 125
  • 128

একটি library-র সর্বশেষ version 1.0.1. পরের release-এ একটি নতুন, ঐচ্ছিক parameter যোগ হবে, যার ডিফল্ট মানে পুরোনো সব ডাক আগের মতোই চলে; কোনো bug fix নেই। SemVer 2.0.0 অনুযায়ী পরের version কোনটি?

  • 1.0.2
  • 2.0.0
  • 1.1.1
  • 1.1.0

একটি ছোট project-এর task: T1 ১ দিন; T2 ৩ দিন (T1-এর পরে); T3 ২ দিন (T1-এর পরে); T4 ১ দিন (T2 আর T3 দুটির পরে)। T3-এর float কত, আর project-এর সবচেয়ে কম সময় কত?

  • Float ১ দিন, project ৫ দিন
  • Float ০, project ৫ দিন
  • Float ১ দিন, project ৭ দিন
  • Float ২ দিন, project ৪ দিন

একটি দলের এক মাসে ৮টি deployment, যার ২টির পরে rollback বা hotfix লেগেছিল। আরেকটিতে পরে একটি ছোট লেখার ভুল ধরা পড়ে, যা পরের নিয়মিত release-এ ঠিক হয় (আলাদা hotfix ছাড়া)। প্রচলিত সংজ্ঞায় change failure rate কত?

  • ৩৭.৫% (৩ ÷ ৮)
  • ২৫% (২ ÷ ৮)
  • ৩৩.৩% (২ ÷ ৬)
  • ৭৫%

Rebase-এর পরে নিজের feature branch push করতে গেলে “rejected (non-fast-forward)” এল। Branch-টি এখনো merge হয়নি, আর একজন সহকর্মী সেখানে কোনো commit push করেছেন কি না নিশ্চিত নন। সবচেয়ে নিরাপদ পদক্ষেপ কোনটি?

  • git push --force
  • git pull, তারপর আবার push
  • git reset --hard origin/feature-branch
  • git push --force-with-lease

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. প্রতিটি ধাপের একটি artefact: stakeholder ও Given-When-Then, সংখ্যাসহ NFR, story point ও critical path, class diagram ও ADR, README ও CONTRIBUTING, test, PR review, CI, CHANGELOG ও tag, bug report, retrospective-এর সংখ্যা।
  2. Repository-র ভিত্তি প্রথম দিনেই: .gitignore, CI, তারপর branch protection; Conventional Commits; main-এর প্রতিটি commit সবুজ — তাতেই পরে bisect সহজ।
  3. দুজনের কাজ: ছোট feature branch, লেবেলসহ review, নিজের অপ্রকাশিত branch-এ git rebase main, conflict-এর সমাধান থেকে নতুন আচরণ আলাদা, --force-with-lease, তারপর --no-ff — first-parent ইতিহাস = PR-এর তালিকা।
  4. Release আর তার পরে: annotated tag, মানুষের জন্য CHANGELOG, SemVer-এর যুক্তি; bug এলে আগে regression test, git bisect run, test-সহ fix, patch release আর tag --contains দিয়ে প্রভাবিত release.
  5. Retrospective সংখ্যা দিয়ে: Git-এর সময় থেকে lead time আর review-এর অপেক্ষা, DORA-ধাঁচের চার সংখ্যা আয়না হিসেবে, burndown-এর আকার, মালিকসহ action item আর risk register — আর অলিখিত requirement সবচেয়ে বড় শিক্ষা।

পরের পর্বে: Delivery, project management আর পেশাদারির মডিউল এখানে শেষ, আর শেষ মডিউল শুরু — পরীক্ষা ও ইন্টারভিউ প্রস্তুতি। পর্ব ৬৬-এ (Numerical সমস্যার অনুশীলন) COCOMO, function point, CPM-PERT আর complexity-র অঙ্ক, পূর্ণ সমাধানসহ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান