07

محرّك النسب

إعادة تشغيل الـ commits، والبصمات، وحالة النسب ودرجة الثقة

5 دقائق قراءة9 أقسام

محرّك النسب يقرر من أدخل كل مخالفة. موجود في src/attribution/، وهو جوهر ما يميّز CleanLens عن الأدوات المبنية على blame.

الفكرة في فقرة واحدة

لكل commit محلل، تحلل CleanLens كل ملف تغيّر قبل الـ commit (نسخة الأب) وبعده، وتعطي كل مخالفة بصمة، ثم تقارن المجموعتين. البصمات الجديدة أدخلها الـ commit؛ والبصمات التي اختفت أصلحها؛ والمشتركة كانت موجودة سابقاً. بعد انتهاء المرور، يُبحث عن كل مخالفة في الكود الحالي ببصمتها لمعرفة الـ commit (والمطوّر) الذي أدخلها.

الخطوة 1 — المرور على الـ commits ‏(commitAttribution.ts)

attributeCommits(root, config, opts):

  1. يسرد الـ commits التي سيُعاد تشغيلها (أحدث N غير دمجية، من الأقدم للأحدث؛ انظر 04 — طبقة Git).
  2. يعالج حتى 12 commit بالتوازي (COMMIT_CONCURRENCY). معظم العمل هو انتظار git، لذلك يساعد التوازي كثيراً.
  3. لكل commit يستخدم النتيجة المخزنة إن وُجدت؛ وإلا يحسب مساهمة الـ commit (الخطوة 2) ويخزنها.
  4. يطبّق المساهمات من الأقدم للأحدث بشكل صارم، حتى يعكس سجل الأصول دائماً آخر حالة.
  5. إذا أُلغي التشغيل، يتوقف عند أول مساهمة ناقصة ويعلّم النتيجة aborted.

الخطوة 2 — commit واحد ‏(computeCommit)

text
parent (or empty tree) ──git diff──▶ changed files + added/removed line ranges
                                          │ filter
                                          ▼
                 analyzable, not excluded, not binary, not whitespace-only
                                          │ git cat-file --batch
                                          ▼
                            before blob          after blob
                                │                     │
                         analyzeRevision()     analyzeRevision()
                                │                     │
                         fingerprints (B)      fingerprints (A)
                                  \                   /
                                   compare the two sets

تُتخطى الملفات إذا كانت ثنائية، أو ليس لها محلل، أو تطابق قائمة الاستثناءات، أو كان تعديلها مسافات فقط (لا يوجد مدى أسطر مع -w)، أو كانت ترويستها تقول إنها مولَّدة (عندما تكون excludeGenerated مفعّلة).

لكل ملف متبقٍّ:

  • الأسطر المحللة += عدد الأسطر في مدى added للـ commit. هذا هو "الكود الذي كتبه المطوّر" الذي تُقسم عليه الدرجات.
  • تُحلَّل نسختا ما قبل وما بعد. تُخزَّن النتائج حسب معرّف الـ blob طوال التشغيل، فالنسخة التي يتشاركها commitان متتاليان تُحلَّل مرة واحدة فقط.
  • ثم تُقارن المجموعتان:
البصمة موجودة في…التصنيفالأثر
ما بعد فقطintroduced (أُدخلت)تُحسب على الكاتب (إذا كانت الثقة high/medium)
ما قبل وما بعدexisting (موجودة سابقاً)تُعدّ، ولا تُحسب على أحد أبداً
ما قبل فقطfixed (أُصلحت)تُحسب لصالح الكاتب (fixedWeighted)

إذا تعذّر تحليل نسخة ما بعد (خطأ صياغة أو blob مفقود)، تُعدّ كل مخالفات نسخة ما قبل غير منسوبة (unattributed) لذلك الـ commit.

الخطوة 3 — ثقة النسب

كل مخالفة أُدخلت تأخذ درجة ثقة من classifyConfidence():

الثقةمتىتدخل في الدرجة؟
highالملف جديد (مضاف، أو منسوخ، أو commit جذري)، أو سطر بداية المخالفة داخل مدى متغيّر، أو امتداد أسطرها يتقاطع مع مدى متغيّر✅
mediumليست على أسطر متغيرة، لكنها تنتمي لرمز مسمّى (دالة/class) والـ commit غيّر أسطراً في الملف✅
lowلا دليل على أن التعديل سبّبها (مثلاً تعذّرت قراءة نسخة الأب لملف موجود)❌ تُعرض فقط

فقط high وmedium تُحسبان (SCORING_CONFIDENCES في scoringConfig.ts).

لماذا هذا مهم، مع أمثلة:

  • يضيف أحمد 60 سطراً إلى دالة موجودة ← صارت تخالف maxFunctionLines. امتداد الدالة يتقاطع مع الأسطر التي أضافها أحمد ← high، تُحسب على أحمد.
  • تضيف ليلى if واحدة في عمق دالة، فتتجاوز الدالة حد التعقيد. النتيجة تبدأ عند ترويسة الدالة (لم تتغير)، لكن امتدادها يتقاطع مع سطر ليلى ← high، تُحسب على ليلى.
  • يعيد خالد تنسيق ملف. مع -w لم يتغير شيء ← يُتخطى الملف. لا أحد يرث أي شيء.
  • تعيد رنا تسمية utils.js إلى helpers.js دون تعديله. يكشف -M إعادة التسمية، لكن البصمات تتضمن المسار، فتبدو كل مخالفة جديدة تحت المسار الجديد. إعادة التسمية لم تغيّر أي سطر (لا يوجد مدى added)، فتأخذ كل مخالفة ثقة low ولا تُحسب على رنا. آثار جانبية في التنفيذ الحالي: بصمات المسار القديم تُعدّ fixed في إجماليات رنا المعروضة (عدد المُصلحة وصافي الأثر؛ وليس الدرجة)، وفي التقرير تظهر هذه المخالفات introduced / low مع commit رنا. ويفقد الكاتب الأصلي ارتباطه بها.

الخطوة 4 — البصمات ‏(fingerprint.ts)

البصمة تتعرف على "نفس المخالفة" في نسختين دون استخدام أرقام الأسطر (التي تتغير كلما عدّل أحد شيئاً فوقها):

text
fingerprint = FNV-1a( ruleId + normalizedPath + symbolName + normalizedContext )
  • normalizedPath: شرطات /، بدون ./ في البداية.
  • symbolName: الدالة/الـ class المحيطة، إن كانت معروفة.
  • normalizedContext: أسطر الكود من startLine إلى min(endLine, startLine + 2) (3 أسطر كحد أقصى)، بعد حذف تعليقات // و# وضم المسافات.

النتائج:

  • إدراج أسطر فوق المخالفة ← نفس البصمة (تبقى existing).
  • تعديل المسافات أو التعليقات في تلك الأسطر ← نفس البصمة.
  • تعديل الكود في أول أسطر المخالفة ← بصمة جديدة: القديمة fixed والجديدة introduced بنفس الـ commit. يُحسب على الكاتب وله؛ والأثر الصافي محايد تقريباً.
  • إعادة تسمية الدالة المحيطة ← بصمة جديدة (تغيّر الرمز).

الخطوة 5 — الدمج مع الكود الحالي ‏(attributeReport.ts)

بعد المرور، تأخذ كل مخالفة من فحص HEAD حالتها النهائية:

الشرطالحالةالثقةdeveloperId
المسار يطابق قائمة الاستثناءاتexcluded—لا يوجد
تعذّرت قراءة الملف من القرصunattributedlowلا يوجد
آخر أصل للبصمة هو introducedintroducedثقة الأصلالكاتب إذا كانت high/medium
غير ذلك (أقدم من النافذة، أو لم تظهر في المرور)existinglowلا يوجد

الحقول introducedBy وintroducedCommit وintroducedAt تُملأ كلما كان الأصل معروفاً، حتى مع الثقة المنخفضة، لتستطيع رؤية الـ commit.

الخطوة 6 — الإجماليات لكل مطوّر

لكل مطوّر (يُطابَق بالبريد بأحرف صغيرة ← معرّف المطوّر)، يجمع المحرّك عبر الـ commits الخاصة به:

الحقلالمعنى
analyzedLinesالأسطر التي أضافها/غيّرها في الملفات القابلة للتحليل
introducedWeighted[category]مجموع أوزان المخالفات التي أدخلها بثقة high/medium، لكل فئة
fixedWeightedمجموع أوزان المخالفات التي أزالتها الـ commits الخاصة به
counts.introducedكل ما أُدخل (بأي ثقة)
counts.introducedScoredما أُدخل بثقة high/medium
counts.fixed وcounts.existing وcounts.unattributedحسب تصنيف كل commit

لاحظ أن هذه أعداد تاريخية: تشمل مخالفات أُدخلت ثم أُصلحت لاحقاً. لذلك قد يكون عدد "المخالفات الجديدة" لمطوّر أكبر من عدد مخالفاته الباقية في الكود.

هذه الإجماليات هي مدخلات الدرجة: انظر 08 — التقييم.

الضمانات والحالات الخاصة

الحالةالسلوك
commit جذري (بلا أب)يُقارن مع الشجرة الفارغة؛ كل مخالفة introduced بثقة high
commits الدمجتُتخطى (--no-merges)
تغيير في المسافات فقطيُتجاهل
إعادة تسميةتُكشف بـ -M؛ من أعاد التسمية لا يُحاسب (ثقة low)، لكن إعادة التسمية تقطع الارتباط بالكاتب الأصلي وتضيف رصيد "fixed" لمن أعاد التسمية (للعرض فقط)
نسخيُكشف بـ -C؛ ويُعامل كملف جديد، فيُحاسب الناسخ (ثقة high) على المخالفات في النسخة
ملف محذوفمخالفاته تُعدّ fixed لمن حذفه
كاتب غير موجود في قائمة المطورينيُحسب في إجماليات المشروع، ولا يُحسب لأي مطوّر
مخالفة أقدم من النافذةexisting؛ لا تُحسب على أحد أبداً
تشغيل ملغىالمساهمات بعد الفجوة تُتجاهل

الاختبارات التي تثبّت هذا السلوك

test/integration.test.ts ينشئ مستودعات Git مؤقتة حقيقية ويتحقق من أن: المخالفة الموجودة سابقاً لا تُحسب على من عدّل الملف لاحقاً؛ والمخالفة على أسطر معدّلة تُحسب؛ والمخالفة المحذوفة تُسجَّل كمُصلحة؛ وإعادة تنسيق المسافات لا تنقل شيئاً؛ والملفات المولَّدة والمستثناة تُتجاهل؛ والـ commit الجذري آمن؛ والمساهم الصغير النظيف لا يحصل على 100 ولا يدخل الترتيب.