تخطَّ إلى المحتوى
kazma.
البحث في كاظمه والتوثيق
EN

عمليات معالجة المستندات

معالجة المستندات — التشغيل والمقياس

Section titled “معالجة المستندات — التشغيل والمقياس”

السطح التشغيلي لـمنصة ذكاء المستندات (بقايا المرحلة 9 وما بعدها). كل شيء مدفوع عبر خدمة الاستيراد المشتركة DocumentIngestionService؛ لا يوجد مُحلِّل ثانٍ ولا مسار كتابة موازٍ.

مواضيع ذات صلة: مسارات الـ API · أمن المستندات · خريطة المراحل · قائمة فحص الإنتاج · مصفوفة الدخان · دليل منتج ذكاء المستندات


جاهزية تحليل PDF / OCR (للمشغّلين)

Section titled “جاهزية تحليل PDF / OCR (للمشغّلين)”

تستخدم ملفات PDF الإلكترونية مقارنة أداء متعددة المحركات بنظام نقاط (bake-off) (PyMuPDF أساسًا ← pypdfium2 اختياريًا ← pdfplumber ← pypdf). الطبقات الممسوحة ضوئيًا / الفارغة / ذات نمط العرض التقديمي تُوجَّه إلى OCR معزول عبر Tesseract (ترتيب تلقائي ara+eng). التفاصيل الكاملة على مستوى المنتج: ذكاء المستندات ← استخراج نص PDF.

الفحصالإشارة السليمة
GET /api/documents/health (قائمة القدرات في صفحة المستندات)مُحلِّل PDF جاهز عند تثبيت PyMuPDF أو pdfplumber
pypdfium2 / pypdf فقطالمُحلِّل مُتدهور (نص فقط — الجداول لا تُعلَن)
PDF إلكتروني عربيرموز بالترتيب المنطقي؛ بيانات IR الوصفية extractor / extraction_score
مسح ضوئي عربيمحرّك OCR العصبي (documents.ocr.engine: يستخدمه auto بمجرد تثبيته، انظر أدناه)، وإلا فـ Tesseract + بيانات traineddata الخاصة بـara على PATH (لا تُقرأ الأرقام العربية الهندية)
بعد kazma updateاستيرادات CLI تعمل (kazma serve يشتغل)؛ انظر تحديث كاظمه

اعتماديات النشر: pip install -e ".[document-platform]" (pymupdf + pypdfium2) وTesseract على مستوى النظام مع حزمتي eng + ara عند الحاجة إلى OCR. وفي نمط الحاوية تكون هذه محرّكات صورة العامل لا محرّكات المضيف: فيُبلغ /api/documents/health حينئذٍ عمّا تملكه الصورة.

الجداول في الصفحات الممسوحة ضوئيًا تُقرأ خلية خلية (documents.ocr.tables، مفعّل افتراضيًا). تُكتشف الشبكة المسطّرة على الصفحة من خطوطها، وتُقرأ كل خلية وحدها، وخلية الرقم التي يقرؤها Tesseract هراءً تُقرأ مرة أخرى أرقامًا. يصبح الجدول كتلة جدول واحدة، خلاياها من اليمين إلى اليسار في الجدول العربي. يكلّف ذلك نحو استدعاء Tesseract واحد لكل خلية (صفحة محتويات من 33 صفًا استغرقت ~20 ث)؛ وبحدٍّ أقصى 600 خلية و120 ث للصفحة، تحتفظ الصفحة بعدها بقراءتها هي. الجداول بلا خطوط تسطير، والجداول الأضيق من نحو خُمس الصفحة، والخلايا المدموجة عبر الخطوط تحتفظ بقراءة الصفحة. قيس على مسح ضوئي حقيقي بدقة 300 dpi لكتيّب عربي: ارتفعت أرقام الصفحات المقروءة في جدول محتويات من 22 % إلى 91 %، وعاد من 3 % إلى 56 % من صفوفه كاملًا على سطر واحد. الأرقام العربية الهندية (١٢٣) لا يقرؤها نموذج Tesseract العربي، السريع منه أو الأفضل (best)، بأي وضع لتقسيم الصفحة وبأيٍّ من الخطّين المضمّنين؛ أما الأرقام الأوروبية فتُقرأ. والمحرّك العصبي يقرؤها.

محرّك OCR العصبي (العربية والأرقام العربية الهندية)

Section titled “محرّك OCR العصبي (العربية والأرقام العربية الهندية)”

documents.ocr.engine (الإعدادات ← المستندات، ويُتحقق منه عند كل كتابة): auto (الافتراضي) يقرأ الصفحات الممسوحة بالمحرّك العصبي حين يكون مثبّتًا هنا (أو في صورة العامل، في نمط الحاوية) وبـ Tesseract خلاف ذلك؛ وneural مثله، ويقول على المستند حين اضطُرّ Tesseract إلى قراءته؛ وtesseract لا يستخدم المحرّك العصبي أبدًا. يقول /api/settings/documents و/api/documents/health أيّ محرّك يقرأ الصفحات (selected_engine) ولماذا لا يستطيع المحرّك العصبي ذلك.

ثبّته على مضيف (نمط process):

نافذة طرفيّة
uv pip install -e ".[ocr]" # onnxruntime, opencv-python-headless, pyclipper, numpy
python scripts/ocr_models.py install # about 240 MB into <data dir>/models/ocr
python scripts/ocr_models.py verify

يُنزّل install النماذج الأربعة المحدَّدة الإصدار (pinned) (يُتحقق من كلٍّ منها مقابل حجمه وSHA-256)، ويحوّل أداتَي التعرّف في EasyOCR المبنيتين على PyTorch إلى ONNX (يُتحقق منهما مقابل PyTorch على مدخلات ثابتة)، ويكتب manifest.json. يحتاج التحويل إلى PyTorch مرة واحدة؛ ومن دونه في هذا المفسِّر يشغّله السكربت عبر uv run --isolated (عجلات CPU على Linux)، فلا تكسب بيئة كاظمه نفسها شيئًا. ينقل KAZMA_OCR_MODEL_DIR دليل النماذج. لا يستخدم عامل المستندات النماذج إلا حين تطابق البيان (manifest)، ولا يُنزّل شيئًا. لا يُنسخ الدليل احتياطيًا ولا ينقله kazma migrate (يُعاد تثبيته).

وفي نمط الحاوية تحمل صورة العامل المحرّك: مرحلة البناء ocr-models فيها (deploy/document-worker/ocr-models.txt، مقفلة بالبصمات، PyTorch لـ CPU) تشغّل المثبِّت نفسه، ولا يصل إلى الصورة النهائية إلا النماذج (/opt/kazma/ocr-models، للقراءة فقط). يسمّي تقرير قدرات الصورة المحرّكين كليهما؛ أعد بناء الصورة لتحديث النماذج.

قيس في 2026-10-15 (على CPU فقط؛ الأرقام في docs/plans/DOCUMENTS_INDUSTRIAL_HARDENING_GOAL.md، Q02): على مجموعة الدقة (fidelity corpus) انتقلت الصفحة العربية النظيفة من استرجاع رموز 0.66 / CER 0.24 / أرقام 1 من 3 (Tesseract) إلى كل كلمة وكل رقم (CER 0)؛ وعلى مجموعة من 12 خطًا لأسطر عربية فيها أرقام، قُرئت الأرقام 27 % -> 74 % نظيفةً و28 % -> 69 % كمسح ضعيف؛ وعلى المسح الحقيقي (خمس صفحات من كتيّب عربي)، مصطلحات صفحة الغلاف 30 % -> 90 %، وأرقام صفحات جدول المحتويات 91 % -> 100 % وصفوفه الكاملة 56 % -> 75 %، بينما فقدت ثلاث صفحات نصية مصطلحات (85 -> 80 %، 100 -> 94 %، 86 -> 71 %) وفقدت صفحة واحدة الرقم الوحيد الذي قرأه Tesseract. وله كلفة في الوقت والذاكرة: على نواة واحدة (حدّ العامل) نحو 15 ث لصفحة نص عربي و30–45 ث لصفحة فيها جداول مسطّرة، مقابل 2–8 ث، ونحو 800 MB من الـ 1 GiB المتاحة للعامل. قرب حدّ وقت العامل (documents.workers.timeout_seconds، 300 ث) تُقرأ الصفحات المتبقية بـ Tesseract وتقول تحذيرات المستند من أي صفحة: ارفع الحدّ (نحو 45 ث لكل صفحة ممسوحة) لإبقاء مسح طويل على المحرّك العصبي.


أسطح المشغّل في اليوم الثاني (Day-2)

Section titled “أسطح المشغّل في اليوم الثاني (Day-2)”
السطحالاستخدام
الويب /documentsالرفع، والمكتبة، والتفاصيل، ولوحة التشغيل (السعة، الجاهزية، التدقيق، جمع القمامة GC)
الإعدادات /settings?tab=documentsأين تُعالَج المستندات غير الموثوقة (عملية أو حاوية)، والإطلاق التدريجي، والاستيراد، وحدود التنفيذ، والحد الأدنى للتخزين، وOCR (ومرور الجداول فيه)، والبرمجيات الخبيثة، وجمع القمامة، والفهرسة
GET /api/documents/healthجاهزية المُحلِّل وOCR والعارض ومُعدِّل PDF، ونمط العزل، ومراجعة السياسة
GET /api/documents/ops/*المقاييس، والسعة، والجاهزية، والاحتفاظ، والتدقيق (التدقيق للمشرف فقط)
POST /api/documents/ops/maintenance/{dry-run,run}جمع القمامة الإداري
python scripts/certify_documents.pyفحص دخان الشهادة / --soak
python scripts/document_pg_backup.pyمخزن Postgres: النسخ الاحتياطي، والتحقق، والاستعادة؛ ومقارنة فهرس المعرفة (index)
python scripts/document_fidelity.pyمجموعة دقة الاستخراج مقابل حدودها الدنيا
python scripts/document_worker_image.pyصورة العامل لنمط الحاوية للكود الموجود على القرص: build، وstatus، وprune

السياسة المحفوظة والفعّالة

Section titled “السياسة المحفوظة والفعّالة”

يقرأ المنسِّق الافتراضي العامل لقطة متّسقة من ConfigStore عند حدّ كل عملية. حدود الاستيراد، وميزانيات المُحلِّل/OCR/العرض، وحدود السعة، وتقطيع الفهرسة، وإعدادات الاحتفاظ/جمع القمامة تنطبق على العملية التالية. تحتفظ العمليات المتزامنة والجارية بسياستها الثابتة التي قُبلت بها؛ وتتشارك الاستدعاءات المتداخلة وخيوط العمّال تلك اللقطة. تقرأ مرحلة العامل السياسة حين تبدأ، وتُفحص المهام المنتظرة من جديد تحت سياسة البرمجيات الخبيثة لتلك المرحلة. ويُفحص رفض التحليل البعيد إضافةً إلى ذلك مباشرةً قبل كل رفع إلى مزوّد، بما في ذلك أثناء عملية جارية بالفعل.

إعدادات الموارد المُنشأة هذه تتطلب إعادة بناء المنسِّق (عادةً إعادة تشغيل الخدمة): storage_root، وquotas.tenant_bytes، وconcurrency للعامل، وlease_seconds، وheartbeat_seconds، وretry_base_seconds، وretry_max_seconds (كلها تحت documents؛ وحقول العامل تحت workers). وتحتفظ بقيمها الفعّالة المثبّتة حتى ذلك الحين. تغيير الجذر لا ينقل البيانات الموجودة ولا يرحّلها. يستخدم النسخ الاحتياطي والترحيل جذر CAS المثبّت، بما في ذلك أثناء تعطّل الإعدادات، فلا يستطيع تغيير جذر معلّق إعادة توجيههما. تتحقق الاستدعاءات غير المتصلة من السياسة المحفوظة دون إنشاء منسِّق. يُبلغ كائن policy في الصحة/الجاهزية عن restart_required، وقيم pending_restart المهيّأة/الفعّالة لكل حقل، وبصمات SHA-256 ‏configured_revision / effective_revision. يخزّن إصدار الاستيراد intake_policy_revision؛ ويخزّن البيان المُطبَّع policy_revision الخاص بتحليله. حدود محاولات إعادة المحاولة تُحفظ عند إنشاء المهمة؛ وتغيير documents.workers.max_attempts (الافتراضي 3؛ والمفتاح الأقدم documents.worker.max_retries لا يزال يُقرأ) ينطبق على المهام الجديدة. البصمة تعرّف القيم المُتحقَّق منها، لا سياسةً موقّعة ولا شهادة نشر.

السياسة الحية غير المتاحة أو غير الصالحة ترفض التنفيذ الجديد بـ document_policy_unavailable؛ وتعيد أسطح HTTP 503 وRetry-After. يرفض حاجز الإطلاق التدريجي الكتابات عند فشل السياسة. وتمسح لوحة التشغيل الحالة الخضراء السابقة حين يفشل التحديث. حقن DocumentConfig الصريح تجاوزٌ ثابت للاختبارات والمستدعين؛ ولا يصبح ربطًا حيًا. تبقى قراءات الإعداد الافتراضية فقط متاحة لمستدعي التوافق/التشخيص، بينما يتطلب التنفيذ اللقطة الصارمة. هذا ليس بعدُ تأهيلًا كاملًا لإعدادات تعدد النسخ أو للنشر.


تُصدَر مقاييس المستندات عبر العرض القياسي لـ Prometheus عند /metrics (لا اعتمادية قياس جديدة؛ تتحلل إلى عمليات فارغة عند غياب prometheus-client). وتتوفر أيضًا لقطة رقمية خالية من المحتوى عند GET /api/documents/ops/metrics.

النطاقالمقاييس
الاستيرادkazma_documents_intake_files_total{outcome}، و…_intake_bytes_total، و…_intake_rejections_total{reason}
الطابور…_queue_depth، و…_queue_oldest_age_seconds، و…_active_leases، و…_retry_waiting، و…_dead_letter_current، و…_dead_letter_total
المراحل…_stage_total{stage,outcome}، و…_stage_latency_seconds{stage}
المُحلِّل/OCR…_parser_total{parser,outcome}، و…_pages_total{kind}
بيئة الحماية…_sandbox_terminations_total{reason} (timeout/oom/output/degraded)
التخزين…_storage_logical_bytes، و…_storage_physical_bytes، و…_storage_dedup_ratio
الفهرسة…_index_latency_seconds، و…_index_chunks_total
التوليد…_generation_failures_total{operation}، و…_redaction_failures_total

المقاييس اللحظية (gauges) لا تُضبط إلا حين تُقرأ مسارات التشغيل: مقاييس الطابور عبر GET /api/documents/ops/metrics أو GET /api/documents/ops/capacity، ومقاييس التخزين عبر GET /api/documents/ops/metrics (تقرأ صفحة المستندات كليهما حين تُفتح وبعد إجراءاتها). لا شيء آخر يحدّثها، فكشط /metrics وحده يُظهر آخر قيمها؛ وعلى إعداد المراقبة الذي ينبّه بناءً عليها أن يستطلع أيضًا GET /api/documents/ops/metrics. أما العدّادات والمدرّجات التكرارية (histograms) فتُحدَّث مع سير العمل.

حصة المستأجر هي واجهة استعلام API، لا وسم Prometheus (كاردينالية وسوم مستأجرين غير محدودة أمر غير آمن). اقرأها من لقطة المقاييس (tenant_quota) أو من GET /api/documents/ops/capacity.

السجلات/التتبعات تحمل معرّفات الارتباط فقط (المستأجر/مساحة العمل/ المستند/الإصدار/المهمة/المحاولة/المُحلِّل) — أبدًا ليست محتوى المستند أو أسماء الملفات أو مصطلحات التنقيح أو الأسرار. تُصدَر مقاطع OpenTelemetry اختيارية عند تثبيت opentelemetry؛ وإلا فهي عمليات فارغة.


كل إجراء موجّه للمشغّل/المستأجر (الاستيراد، والوصول، والفهرسة، والتوليد، والتحويل، والتعديل، والتنقيح، والتنزيل، والإلغاء، وإعادة المحاولة، والحذف، وجمع القمامة) يُلحَق بسجلّ تدقيق غير قابل للتغيير ومقيّد بالمستأجر (document_audit_events). على SQLite يعيش في documents.db؛ وعلى البيانات الوصفية في Postgres يستخدم نفس اسم الجدول في التجمع المشترك. وكلاهما يرفض التحديث وأي حذف عدا حذف اجتياح الاحتفاظ (مشغّلات triggers على SQLite، وdocument_audit_guard على Postgres منذ 2026-10-13؛ انظر أمن المستندات). وهو يكمّل — ولا يكرر — سجلّ أحداث المراحل الخاص بكل مهمة. اقرأ صفحة عبر GET /api/documents/ops/audit?limit=&before_id=&document_id=&event_type=. هذا المسار للمشرف فقط (401 بلا جلسة، و403 للأدوار الأخرى): فالسجلّ يسمّي كل مستند ومهمة وفاعل لدى المستأجر، بما في ذلك مستندات لا يستطيع المستدعي قراءتها. التفاصيل مقيّدة بقائمة سماح من القيم القياسية الآمنة؛ المحتوى لا يدخل التدقيق أبدًا.


الضغط العكسي، والسعة، وحدود المعدل

Section titled “الضغط العكسي، والسعة، وحدود المعدل”

يُرفَض الاستيراد بحالة صادقة عند بلوغ أي حد؛ ورفضات السعة (429/503/507 وintake_in_progress) تحمل أيضًا Retry-After:

الحدالحالةمفتاح الإعداد
الحد الأدنى للمساحة الحرة للتخزين507documents.capacity.storage_free_floor_bytes
سقف الطابور العام503documents.capacity.max_queued_jobs
حد المهام المنتظرة لكل مستأجر429documents.capacity.max_tenant_queued_jobs
حد المهام النشطة لكل مستأجر429documents.capacity.max_tenant_active_jobs
نافذة معدل/بايتات الاستيراد429documents.capacity.intake_rate_per_minute، و…intake_bytes_per_minute
سلطة السعة غير متاحة503فشلت قراءات الطابور/المستأجر أو قياس المساحة الحرة المطلوب
عمليات رفع تُستقبَل في آن واحد (لكل عملية)503 uploads_busydocuments.capacity.max_concurrent_uploads (الافتراضي 4)
طول مُعلَن يتجاوز حدّ الاستيراد413 intake_too_largedocuments.intake.max_bytes (الافتراضي 50 MiB)
حصة تخزين المستأجر413 quota_exceededdocuments.quotas.tenant_bytes (الافتراضي 10 GiB)
الكتابات الدائمة مُطفأة503 document_platform_disableddocuments.enabled
فحص البرمجيات الخبيثة المطلوب لا يمكن تشغيله503 malware_scanner_unavailable، قبل قراءة الجسمdocuments.security.malware_scan = on، أو documents.security.malware_fail_closed
استيراد تجاوز مدة حيازته فاستُولي عليه409 intake_taken_overdocuments.intake.lease_seconds (الافتراضي 300)

يسأل الرفع عبر الويب (POST /api/documents) عن حدود الميزانية — الحد الأدنى للتخزين، وحدود الطابور والمستأجر، ونافذة المعدل، والطول المُعلَن، ومفتاح المنصة، وما إذا كان يمكن تشغيل ماسح برمجيات خبيثة مطلوب — قبل أن يقرأ جسم الطلب: فالرفض يكلّف العميل طلبًا واحدًا، لا عملية رفع كاملة. يحسب الحد الأدنى للتخزين الملف الوارد (المساحة الحرة ناقص الحجم المُعلَن). ثم يُخزَّن الجسم مؤقتًا تحت جذر التخزين (.upload-staging/)، حيث يقيسه الحد الأدنى ويزيل جمع القمامة ما يتركه انهيار؛ وتعيد الخدمة فحص كل شيء عند الاستيراد، وهناك يُطبَّق فحص البرمجيات الخبيثة وحصة تخزين المستأجر.

كل مسار مستندات يجيب رمز الخطأ الواحد بالحالة نفسها (documents_api._STATUS_BY_CODE): الرفض الذي تسببه حالة الخادم — الكتابات مُطفأة، أو لا ماسح، أو لا نمط عامل مسموح — هو 503 مع Retry-After على كل مسار. حتى 2026-10-13 كانت الكتابات المُطفأة تجيب بـ 422 على مسارات الإجراءات، والماسح المطلوب الغائب بـ 400، بعد قراءة الجسم.

مسارات أجسام JSON (/import، و/generate، و/merge، و/convert، و/split، و/fill-form، و/redact، و/index، و/unindex، و/search، و/{id}/delete الذي جسمه اختياري) تقرأ جسمها عبر قارئ محدود بـ 2 MiB: الطول المُعلَن الذي يتجاوزه يُرفض دون قراءة، وغير المُعلَن يُقطع بمجرد أن يتجاوزه (413 request_too_large)؛ وJSON المشوّه، أو الجسم الذي ليس كائنًا، أو التداخل الأعمق مما يحتمله المُحلِّل هو 422 invalid_json.

يحجز الاستيراد الدائم خانات الطابور ونافذة ملفات/بايتات المستأجر في قاعدة بيانات المهام قبل قراءة مصدره. يُسلسل SQLite معاملات الحجز القصيرة بـ BEGIN IMMEDIATE؛ ويقفل PostgreSQL صف قبول مشتركًا. تُحتسب التدفقات المعلّقة ضمن حدود قبول الطابور والمستأجر. والتدفقات مجهولة الحجم تحجز الحد الأقصى المهيّأ لحجم الاستيراد؛ ويجب أن تتطابق الأطوال المُعلَنة قبل نشر الكتلة. والقراءات الناجحة توفّق الحجز مع البايتات الفعلية. تبقى النافذة بعد إعادة تشغيل الخدمة. وإعادات الطلب المتساوية الأثر (idempotent replays) المُتحقَّق منها لا تستهلك رصيد قبول جديدًا، بما في ذلك حين يكون الطابور ممتلئًا. وإعادات المحاولة اليدوية للمهام تحجز السعة أيضًا وتتطلب صلاحية كتابة على مستند حيّ.

يُبلغ GET /api/documents/ops/capacity عن أعداد الحجوزات المعلّقة وبايتاتها، مع degraded_reasons قابلة للقراءة آليًا. وقاعدة بيانات حجز غير متاحة ترفض الاستيراد بـ 503. والمفتاح المحجوز بالفعل يعيد 409 مع intake_in_progress؛ وطلب مختلف يستخدمه يعيد idempotency_conflict. مفاتيح الطلبات محدودة بـ 256 حرفًا. تبحث الإعادة عن زوج المستأجر/المفتاح المخزّن، محافظةً على معرّفات المهام الأقدم. وتُرمِّز المعرّفات الجديدة المستأجر والمفتاح ونوع الكائن قيمًا متمايزة، فلا تستطيع أسماء مستأجرين ومفاتيح تحتوي على فواصل أن يلتبس بعضها ببعض.

لا تنتهي صلاحية الحجوزات بالعمر وحده. يسجّل كل حجز مالكه (التثبيت، والمضيف، ومعرّف العملية ووقت بدئها)، وقبل أي كتابة للبيانات الوصفية، معرّفات المستند والإصدار التي ينشرها؛ وكل صف ينشئه يحمل رمزه (token). والاستيراد الذي يفشل يتراجع عن صفوفه هو. حين تبدأ الخدمة، وكل دقيقة أثناء عمل عمّالها، تتراجع عن الاستيرادات غير المنتهية لمالكين ثبت موتهم — التثبيت والمضيف أنفسهما، والعملية غائبة أو أُعيد استخدام معرّفها — وللحجوزات المستعادة من نسخة احتياطية.

المالك الذي لا يمكن إثبات موته — استيراد بدأ على مضيف آخر، أو عملية لا تستطيع هذه العملية فحصها — يحمل حيازة (lease) ما دام يعمل: يجدّدها كل ربع من documents.intake.lease_seconds (الافتراضي 300 ث، وتُسجَّل على الحجز، فتحكم كل نسخة عليه بإعداد المالك). والحجز الذي انقضت حيازته يُستولى عليه خلال دقيقة ويُتراجع عنه، وتُحرَّر سعته. قد يكون المالك القديم معزولًا شبكيًا أو موقوفًا مؤقتًا فحسب، لذا يُسيَّج (fenced): المهمة التي تنشر استيرادًا لا تُنشأ إلا ما دام الحجز لا يزال له، فلا يستطيع استيراد مُستولى عليه أن ينشر أبدًا. وإن استيقظ، يفشل رفعه بـ intake_taken_over (409) ولا يُحفظ شيء (أعد المحاولة)؛ ويزيل الصفوف التي كتبها، ويزيل الاجتياح أي صفوف تركها، لمدة يوم بعد الاستيلاء.

الحجز الذي كتبه إصدار بلا حيازة (بلا نبض heartbeat) لا يُستولى عليه أبدًا: يبقى محجوزًا ويُحتسب admission.stranded في GET /api/documents/ops/capacity، وتُبلغ الجاهزية عن stranded_intake_reservations بالسبب document_intake_reservations_stranded (والحيازة المنقضية تُحتسب هناك أيضًا حتى يستولي عليها الاجتياح؛ أما الاستيراد البعيد الذي يجدّد حيازته فلا). لا تمسح حجزًا قديمًا إلا بعد التأكد من أن ناشره قد زال.

يُؤهَّل التعافي من الانهيار بمصفوفة تقتل عملية حقيقية بعد كل مرحلة استيراد (الحجز، والبايتات المخزّنة، ونية النشر، والكتلة، والمستند، والإصدار، والمهمة) وداخل كل مرحلة مهمة (التحليل، والتطبيع، والفهرسة، والتحقق)، على SQLite وعلى PostgreSQL حقيقي: يحرّر البدء التالي الحجز، ويزيل ما نشره الاستيراد الميت، ويستولي على الحيازات المنتهية، ويُنهي كل مهمة مرة واحدة (tests/test_document_crash_recovery.py). وتُؤهَّل الحيازة بعملية استيراد حقيقية تُعطى هوية مضيف آخر بلا تجديد فعّال، تُوقَف مؤقتًا ويُستولى عليها ثم تُستأنف، على كلتا الخلفيتين: فتُسيَّج، وتزيل ما كتبته، ويزيل الاجتياح ما كتبته ولم تستطع إزالته (tests/test_document_intake_lease.py).

يأخذ العمّال المهام بإنصاف بين المستأجرين: تأتي المهمة التالية من المستأجر صاحب أقل عدد من المهام الجارية، ثم بحسب وقت جاهزية المهمة (إعادة المحاولة تنتظر دورها). تفرض البيانات الوصفية في PostgreSQL حصة كتل المستأجر داخل معاملة النشر. تتطلب اختبارات السباق على PostgreSQL قاعدة بيانات اختبار مهيّأة صراحةً؛ ومهايئ SQLite لا يؤهّل تزامن PostgreSQL.

الحد الافتراضي للمساحة الحرة هو 512 ميبيبايت؛ وعلى مضيف إنتاج فكّر في ≥ 1 جيبيبايت (الإعدادات ← المستندات أو ConfigStore). ومراحل التحليل تفحص الحد الأدنى أيضًا: المرحلة التي كانت ستبدأ دونه تُعاد محاولتها لاحقًا (storage_full)، ولا تُشغَّل أبدًا على قرص ممتلئ.


الاحتفاظ حي مدعوم بـ ConfigStore ‏(documents.retention.*، وdocuments.gc.*). الجامع آمن ضد الانهيار بنمط الوسم/الاجتياح (mark/sweep) مع قاعدة البيانات الوصفية (SQLite أو Postgres) سلطةً وحيدة:

  • يستردّ الكتل (blobs) اليتيمة/غير المُشار إليها، ونسخ الحجر الصحي بعد الترقية، ومحتوى شواهد الحذف (tombstones) منتهية الصلاحية، ومحتوى الإصدارات الفاشلة نهائيًا (المرفوضة/الرسائل الميتة)، وصفوف الكتل اليتيمة، والبيانات (manifests) اليتيمة، وصفوف التدقيق المتقادمة.
  • أبدًا لا يحذف الكتل المُشار إليها، أو محتوى الإصدار الحالي، أو المصنّفات (إزالة تكرار العنونة بالمحتوى محفوظة).
  • يحترم فترة سماح، ويحدّ عدد الحذف في كل تشغيل (documents.gc.max_deletions_per_run)، ويرفض التتبع أو الحذف عبر رابط رمزي/نقطة وصل أو خارج جذر المخزن.

ويزيل جمع القمامة أيضًا البقايا المؤقتة التي يتركها انهيار — مخازن الرفع المؤقتة (.upload-staging/)، والنسخ المرحلية (.stage/)، وأدلة parser-runs/ / operation-runs/ — بمجرد تجاوز فترة السماح، ولا يحذف أبدًا كتلةً أو بيانًا (manifest) ثبّته نسخ احتياطي جارٍ (.backup-pins/).

تعمل حلقة مجدولة بعد ثلاث دقائق من البدء ثم كل documents.gc.interval_hours (الافتراضي 6) ما دامت documents.gc.enabled وdocuments.gc.auto_maintain والكتابات الدائمة مفعّلة (تُلغى بنظافة عند الإيقاف). يستطيع المشغّلون التشغيل التجريبي ثم التأكيد من صفحة المستندات أو عبر POST /api/documents/ops/maintenance/dry-run و…/run (مقيّد بالمشرف).

البيانات الوصفية في Postgres: عمليات CRUD مبنية لعدة نسخ (غير مؤهّلة بوصفها نشرًا: انظر جاهزية تعدد النسخ) ويُشغَّل جمع القمامة على كلتا الخلفيتين — يوجّه retention._mark إلى repository.gc_mark، الذي تنفذه كلٌّ من repository.py وrepository_pg.py. (الإصدارات الأقدم كانت تتخطى جمع القمامة على Postgres مع gc_postgres_metadata_sql_port_pending؛ أُزيل ذلك الحاجز ويؤكد اختبار أنه لن يعود.)


عمّال المستندات المعزولون (نمط الحاوية)

Section titled “عمّال المستندات المعزولون (نمط الحاوية)”

يشغّل نمط process الافتراضي المُحلِّلات في عملية فرعية محلية بحدود للموارد لكن بلا عزل لنظام الملفات أو الشبكة. للمستندات التي لا تثق بها، شغّل نمط الحاوية: فتعمل كل مهمة تحليل وOCR وعرض وتعديل حينئذٍ في حاوية جديدة مُصدَّق عليها (attested) بلا شبكة وبلا وحدات تخزين مركّبة (mounts). نموذج الأمن في أمن المستندات — الطبقة 3.

الإعدادات ← المستندات ← أين تُعالَج المستندات غير الموثوقة (للمشرف فقط): عملية محلية أو حاوية لكل مهمة. الإعداد هو security.execution.document_worker_isolation. وهو تحت security.، فلا تستطيع أدوات الإعداد لدى الوكيل تغييره، ولا تغيّره استعادة الإعدادات أبدًا. ولا يزال KAZMA_DOCUMENT_WORKER_ISOLATION في البيئة يتقدّم حين يكون مضبوطًا؛ فتُظهر الصفحة حينئذٍ المفتاح معطّلًا وتقول السبب.

حين يتطلب التثبيت الحاويات (security.execution.container_required، ملف التنفيذ الصارم)، يرفض نمط العملية المحلية كل مستند (worker_isolation_required). تقول صفحة الإعدادات ذلك فوق المفتاح وتعرض استخدم نمط الحاوية.

الإعداد الذي لا يمكن قراءته يرفض المستندات (document_policy_unavailable، وتُعاد المحاولة): ولا يرجع أبدًا إلى النمط الأقل عزلًا.

التثبيت الأصلي: الحارس يشغّل الوسيط

Section titled “التثبيت الأصلي: الحارس يشغّل الوسيط”

على تثبيت يشرف عليه الحارس (scripts/service/kazma_guard.py، مهمة KazmaAgent)، لا شيء يلزم تشغيله يدويًا ولا يحتاج .env إلى شيء:

  • كاظمه تطلب، والحارس يشغّله. حين يُختار نمط الحاوية، تكتب كاظمه طلبًا في .kazma/document_broker.request.json (عند الإقلاع، وخلال ثانية من تغيير الإعدادات، وفي مرور الصيانة كل 15 دقيقة). يبدأ الحارس الوسيط خلال ثانية، ويوقفه حين يعود النمط إلى العملية. ويبقى الطلب بعد إعادات التشغيل، فبعد إحداها يبدأ الحارس الوسيط قبل كاظمه، فيُجري التصديق بينما تُقلع كاظمه.
  • الحارس يسلّم كاظمه العنوان. يبدأ كل خادم مع KAZMA_DOCUMENT_BROKER_URL (منفذ loopback يحتفظ به للتثبيت)، وKAZMA_DOCUMENT_BROKER_TOKEN_FILE (.kazma/document_broker.token، يصنعه الحارس مرة واحدة، ويُقصَر على حسابه قبل كتابة السر فيه ثم من جديد عند كل بدء — والملف الذي لا يمكن قصره يوقف الوسيط —، ولا يُسجَّل أبدًا) وKAZMA_GUARD_BROKER_REQUEST_FILE. وملف .env الذي يسمّي وسيطًا آخر يتقدّم؛ فلا يشغّل الحارس حينئذٍ أيّ وسيط.
  • الصورة لهذا الكود. يطلب الوسيط kazma-document-worker:src-<16 hex>، وهو وسم مشتق من كل ما تُبنى منه الصورة (ملف Dockerfile، ومتطلباته، وkazma_core كله). وما دام ذلك الوسم غير مبني، يخدم أحدث صورة صنعها سكربت البناء، ويتوقف للانتقال (الخروج 6، ويُعاد تشغيله فورًا) بمجرد بنائه.
  • يبقى عاملًا. الوسيط الذي يخرج يُعاد تشغيله (5 ث، 15 ث … 5 دقائق)؛ والذي يكفّ عن الاستجابة يُعاد تشغيله بعد ثلاثة مجسّات فائتة. والصورة المفقودة أو محرّك Docker الذي لا يستجيب يُفحص من جديد كل 30 ث، بهدوء. ينبّه الحارس مرة واحدة حين يستمر الوسيط في الفشل (ثلاث محاولات بدء فاشلة متتالية) ومرة حين لا يستجيب Docker لمدة 10 دقائق، ومرة أخرى حين يعود. أما الصورة المفقودة أو القديمة فتنبيه تشغيل من كاظمه بدلًا من ذلك (documents.worker_image_outdated، مرة لكل بناء، مع الأمر).
  • الصيانة. الإيقاف المؤقت (--pause، kazma update) يوقف الوسيط مع كاظمه؛ والبدء التالي يبدؤه من جديد. والوسيط الذي تغيّر كوده أو صورته يُعاد تشغيله حين تُعاد كاظمه.
  • ما يسجّله. يحوي guard.log أحداث broker.* مثل أحداث الحارس نفسه؛ ومخرجات الوسيط في .kazma/document_broker.log؛ ويحمل ملف حالة الحارس document_broker (الحالة، وpid، والصورة، وإعادات التشغيل، وآخر خروج)، الذي تقرؤه الجاهزية؛ ويعرضه kazma_guard.py --status.

يعمل الوسيط بالحساب الذي يشغّل الحارس، ببيئة مُختزلة إلى ما يحتاجه Python وWindows وواجهة docker السطرية (متغيرات الحارس نفسه، ومنها رمز Telegram الخاص به، تبقى خارجها). يستمع على loopback فقط؛ والرمز (token) هو المصادقة الوحيدة هناك.

نقل تثبيت إلى نمط الحاوية

Section titled “نقل تثبيت إلى نمط الحاوية”

في مجلد التثبيت، وبـ Python التثبيت (.venv\Scripts\python.exe على Windows):

  1. ابنِ صورة العامل للكود الموجود على القرص. يستغرق البناء الأول بضع دقائق (المحرّكات)، والتالية ثوانيَ:

    نافذة طرفيّة
    .venv\Scripts\python.exe scripts\document_worker_image.py build
  2. اختر نمط الحاوية: الإعدادات ← المستندات ← استخدم نمط الحاوية (أو المفتاح، ثم حفظ).

  3. أعد تشغيل الحارس مرة واحدة، كما يلي — فقط لأن الحارس العامل اليوم بدأ قبل أن يصبح قادرًا على تشغيل الوسيط. تقول صفحة المستندات وkazma_guard.py --status متى يلزم ذلك.

إعادة التحميل (kazma_guard.py --reload) تعيد تشغيل كاظمه، لا كود الحارس نفسه أبدًا: الحارس هو عملية مهمة KazmaAgent ويعيش من إقلاع إلى إقلاع. لتشغيل حارس أحدث، حين لا تعمل أي دورة محادثة:

نافذة طرفيّة
.venv\Scripts\python.exe scripts\service\kazma_guard.py --pause --stop --when-idle --reason "guard update"
schtasks /End /TN KazmaAgent
schtasks /Run /TN KazmaAgent
.venv\Scripts\python.exe scripts\service\kazma_guard.py --resume

يجعل الإيقاف المؤقت الحارسَ القديم يوقف كاظمه بسلاسة ويبقى ممسكًا بها؛ ويبدأ الحارس الجديد ممسكًا بالإيقاف المؤقت نفسه، ويبدأ الاستئنافُ كاظمه، التي تطلب الوسيط. شغّل سطري schtasks من طرفية بصلاحيات المسؤول إن رُفضت طرفية عادية. وإعادة تشغيل الجهاز تفعل ما يفعله سطرا schtasks.

ومن ذلك الحين: تبديل النمط هو صفحة الإعدادات وحدها، وتحديث الكود هو المعتاد kazma_guard.py --reload --when-idle، وبعد تحديث تطلب صفحة المستندات (وتنبيه التشغيل) الخطوة 1 من جديد حين تكون الصورة أقدم من الكود — ويواصل الوسيط خدمة الصورة الأقدم حتى تُبنى، ثم ينتقل من تلقاء نفسه.

إعدادات الحارس نفسه، في بيئته (بيئة مهمة KazmaAgent، لا .env): KAZMA_GUARD_DOCUMENT_BROKER (auto يتبع طلب كاظمه، وon يشغّل وسيطًا دائمًا، وoff لا يشغّل أبدًا)، وKAZMA_GUARD_BROKER_PORT (تثبيت المنفذ)، وKAZMA_GUARD_BROKER_IMAGE (تثبيت صورة؛ بلا رجوع حينئذٍ).

يقول python scripts/document_worker_image.py status أيّ وسم يحتاجه هذا الكود، وأيّها مبني، وماذا يخدم وسيط الحارس؛ ويزيل ... prune البناءات الأقدم (لا يزيل أبدًا صورة هذا الكود ولا الصورة التي تخدم).

نافذة طرفيّة
docker build -f deploy/document-worker/Dockerfile -t kazma-document-worker:local .
نافذة طرفيّة
mkdir -p secrets
python -c "import secrets; print(secrets.token_urlsafe(48))" > secrets/document_broker_token
نافذة طرفيّة
docker compose -f docker-compose.yml -f deploy/docker-compose.document-broker.yml up -d

يتجاهل git وكل بناء صورة المجلد secrets/. يبني التراكب (overlay) خدمة document-broker (deploy/document-broker/Dockerfile) ويضيفها — وهي الحاوية الوحيدة التي تركّب /var/run/docker.sock، وهي نفسها بلا شبكة، وبجذر للقراءة فقط وبلا قدرات (capabilities) — ويضبط على kazma كلًّا من KAZMA_DOCUMENT_WORKER_ISOLATION=container، وKAZMA_DOCUMENT_BROKER_URL (مقبس unix على وحدة تخزين يتشاركانها) وKAZMA_DOCUMENT_BROKER_TOKEN_FILE. يجب أن يكون الرمز 32 حرفًا على الأقل. لا تركّب مقبس المحرّك في kazma نفسها أبدًا.

من دون الحارس (خادم مطوّر، أو مشرف آخر)، شغّل python -I kazma-core/kazma_core/documents/worker_broker.py --image <tag> --token-file <file> --listen tcp:127.0.0.1:8765 تحت حساب يُسمح له باستخدام Docker (أضف --fallback-to-newest-built لخدمة أحدث بناء ما دام <tag> مفقودًا)، واضبط KAZMA_DOCUMENT_BROKER_URL=tcp://127.0.0.1:8765 وKAZMA_DOCUMENT_BROKER_TOKEN_FILE=<file> لكاظمه. الرمز هو المصادقة الوحيدة على TCP: يستمع الوسيط على loopback ما لم يُبدأ بـ --allow-non-loopback. رموز خروجه: 0 طُلب منه التوقف، و2 لم يستطع البدء، و3 لم تصمد السياسة أو لم تُبلغ الصورة عن محرّكات، و4 لا صورة عامل، و5 Docker لا يستجيب، و6 ظهرت الصورة المطلوبة.

  • python -I kazma-core/kazma_core/documents/worker_broker.py --image <tag> --token-file <file> --attest-only يطبع التصديق وتقرير محرّكات الصورة (الخروج 0 = السياسة صامدة والصورة أبلغت عن محرّكاتها، و3 = ليست كذلك، مع تسمية كل مشكلة، و2 = لم يستطع الوسيط البدء).
  • GET /api/documents/ops/readiness ← worker_isolation: mode، وready، وfilesystem_isolation، وnetwork_isolation، وhostile_document_profile (container_broker حين يكون جاهزًا؛ وunqualified لنمط process)، وimage_code_matches، وimage_current (الصورة التي تخدم بُنيت من هذه النسخة المسحوبة checkout)، وimage_fallback (بناء أقدم يخدم)، وbroker (الصورة، ومعرّف الصورة، والحدود، وattested، وattested_at، وproblems، وactive، وquarantined)، وselection (أيّ نمط وما الذي اختاره: البيئة، أو الإعداد، أو الافتراضي)، وsupervisor (من يشغّل الوسيط — الحارس، أو حارس أقدم، أو وسيط آخر، أو لا أحد — وسجلّ الحارس نفسه عنه) وaction: الخطوة الواحدة التي تصلح ما ليس سليمًا (code، وmessage بالإنجليزية، وcommand المطلوب تشغيله)، أو null. ونمط الحاوية غير الجاهز — أو نمط process بينما security.execution.container_required مفعّل — يضيف document_worker_isolation_unavailable إلى degraded_reasons.
  • تُظهر صفحة المستندات الشيء نفسه: النمط، وحالة الوسيط، وصورة أقدم تخدم، والخطوة مع أمرها (باللغتين).
  • يسجّل الوسيط سطرًا واحدًا لكل مهمة: العامل، والحالة، ورمز الخروج، وبايتات المخرجات، وcleanup=verified|failed والمدة.
  • جاهزية المحرّكات تأتي من الصورة. عند البدء يسأل الوسيط الصورة أيضًا عن المُحلِّلات والعارضات ومُعدِّلات PDF ولغات OCR التي تملكها، ويرفض الخدمة دون ذلك التقرير. في نمط الحاوية يقرأ GET /api/documents/health لدى كاظمه وفحصُ المحرّك في كل عملية ذلك التقريرَ، لا حزم المضيف نفسه أبدًا: فالمضيف الذي يفتقد LibreOffice أو WeasyPrint لا يرفض ما تستطيعه الصورة، والمحرّك الذي تفتقده الصورة يُقرأ غير متاح مع سبب الصورة.
  • الصورة القديمة مرئية. تُبلغ الصورة عن بصمة لكود المستندات فيها، ويضع سكربت البناء عليها وسم بصمة كل ما بُنيت منه. image_code_matches: false أو image_current: false أو image_fallback: true (يضيف كلٌّ منها سبب الجاهزية document_worker_image_outdated، ويفشل فحص worker_isolation لدى أداة الاعتماد) يعني أن الصورة التي تخدم غير معروف أنها تشغّل الكود الذي تشغّله كاظمه هذه: python scripts/document_worker_image.py build.
الرمزالمعنىالإجراء
worker_isolation_unavailableالوسيط غير مهيّأ، أو لا يمكن الوصول إليه، أو غير مُصدَّق عليه، أو رُفض رمزه، أو ضاع التشغيلتسمّي الجاهزية الخطوة (worker_isolation.action)؛ وتُعاد محاولة المهمة ضمن ميزانية الطابور المحدودة
worker_isolation_busyكانت كل خانات الوسيط مشغولة طوال انتظار القبولارفع --concurrency (وسعة المضيف) أو دع إعادات المحاولة تمتص الدفعة
worker_isolation_invalidKAZMA_DOCUMENT_WORKER_ISOLATION ليس process ولا containerأصلح الإعداد؛ يُرفض كل تشغيل حتى ذلك الحين
worker_isolation_requiredsecurity.execution.container_required مفعّل والمستندات تعمل في نمط processالإعدادات ← المستندات ← استخدم نمط الحاوية (أو أزل KAZMA_DOCUMENT_WORKER_ISOLATION=process من البيئة)
document_policy_unavailableتعذّرت قراءة إعداد النمط (أو سياسة التنفيذ)تُعاد المحاولة؛ وتعطّل مخزن الإعدادات نفسه يُبلَغ عنه في مكان آخر
document_engine_unavailableصورة العامل نفسها تفتقد المحرّك لهذه العملية (تقريرها يقول السبب)أعد بناء صورة العامل مع المحرّك؛ حزم المضيف لا تُحتسب
worker_output_invalidأعاد العامل تدفقًا يخرق البروتوكول (الأسماء، والأحجام، والبصمات)دائم؛ عامِل المستند على أنه عدائي
parser_oomقُتلت حاوية المهمة بحدّ ذاكرتهاتُعاد المحاولة ضمن الميزانية؛ ارفع --memory-mb فقط إن بلغته مستندات مشروعة
parser_timeout / parser_output_limitبُلغت المهلة أو سقف المخرجات؛ قُتلت الحاوية وأُزيلتكما في نمط العملية

cleanup=failed في سجل الوسيط يعني أن حاوية مهمة تعذّرت إزالتها؛ وتبقى خانتها خارج الخدمة حتى تزيلها محاولة لاحقة، وتُبلغ الجاهزية عن quarantined.

تُثبَّت حدود الوسيط حين يبدأ (--memory-mb 2048، و--cpus 1، و--pids 256، و--tmpfs-mb 768، و--concurrency 2، و--max-timeout 600 ث، و--max-input-mb 640، و--max-output-mb 320)؛ ولا تستطيع كاظمه رفعها. يحمل /tmp مدخلات المهمة وملفاتها المؤقتة ومخرجاتها، وتُحتسب صفحاته ضمن حدّ الذاكرة، لذا يجب أن يغطي --tmpfs-mb أكبر دمج (documents.intake.max_files × documents.intake.max_bytes) مع مخرجاته، ويجب أن يتجاوز --memory-mb قيمة --tmpfs-mb بمقدار ذاكرة عمل المُحلِّل. تدفع كل مهمة كلفة بدء حاوية وإزالتها (0.35–0.8 ث على مضيف التأهيل).

مهمة CI ‏Document worker container profile (real Docker) تبني الصورتين كلتيهما، وتصدّق على السياسة، وتشغّل مجموعة التأهيل (tests/test_document_worker_container_docker.py، مع KAZMA_TEST_DOCUMENT_WORKER_IMAGE الذي يسمّي الصورة؛ والتخطي يُفشل المهمة) والطوبولوجيا بشكل compose (python scripts/document_broker_topology.py <worker image> <broker image>). شغّل الأمرين نفسيهما على مضيف نشر لتأهيله. وعلى تثبيت أصلي، يشغّل tests/test_document_broker_guard_docker.py (المتغير نفسه) الحارس والوسيط الحقيقيين: ملف PDF حقيقي يُحلَّل عبر الحاوية، والوسيط يُقتل ويعيده الحارس، ولا يبقى شيء خلفه.

التثبيت الأصلي: python scripts/document_worker_image.py build بعد git pull وقبل إعادة التحميل (kazma_guard.py --reload --when-idle)، لكي تبدأ كاظمه على الصورة الخاصة بكودها؛ فإعادة التحميل أولًا تعمل على الصورة القديمة وتطلق تنبيه “worker image outdated” إلى أن يتم البناء. وإن بُنيت لاحقًا، ينتقل وسيط الحارس إلى الصورة الجديدة خلال دقيقة، بلا إعادة تشغيل. يعيد kazma update تشغيل كاظمه بنفسه، فابنِ معه مباشرةً بعده (تنبيه واحد). وتُسمّى الصورة بحسب كل ملف من kazma_core يرسله البناء، فأيّ تغيير هناك يحتاج إلى بناء، لا تغيير كود المستندات وحده. لتدوير الرمز، احذف .kazma/document_broker.token وأعد تحميل كاظمه: يصنع الحارس رمزًا جديدًا ويعيد تشغيل الوسيط. Compose: يثبّت الوسيط صورة العامل بمعرّفها عند البدء، لذا بعد إعادة بناء صورة العامل، أعد تشغيل document-broker؛ ويُقرأ ملف الرمز في كل تشغيل من جهة كاظمه، وعند البدء من جهة الوسيط: اكتب الرمز الجديد، ثم أعد تشغيل document-broker.

يشغّل الاستيراد ClamAV عند تهيئته (documents.security.malware_scan = auto/on/off، الافتراضي auto)، وتفحص كل مرحلة تحليل الأصلَ من جديد تحت سياسة تلك اللحظة. يظهر المجس في الجاهزية (malware) وفي الإعدادات ← المستندات.

يكون الفحص مطلوبًا حين يكون الوضع on أو يكون documents.security.malware_fail_closed مفعّلًا — وافتراضيه مفعّل مع KAZMA_PRODUCTION=1، فمضيف إنتاج بلا clamscan/clamdscan يرفض عمليات الرفع. حين لا يمكن تشغيل فحص مطلوب، تُبلغ الجاهزية عن malware_scanner_unavailable مع accepting_durable_writes: false، ويُرفض الرفع الجديد (malware_scanner_unavailable)، وتُمسك المهمة المنتظرة بالفعل وتُعاد محاولتها ضمن ميزانيتها المحدودة (documents.workers.max_attempts) ثم تُرسل إلى الرسائل الميتة — فالتعطّل لا يُسجَّل أبدًا على أنه محتوى غير آمن. وتحت auto بلا الإغلاق عند الفشل، يُسجَّل الماسح المفقود أو الفاشل في السجل ويُقبل المستند دون فحص. انظر أمن المستندات.

لا يكون ClamAV أفضل من تواقيعه. يقرأ المجس تاريخ بنائها من clamscan --version (malware.signatures: ‏built وage_days وstale). والتواقيع التي يتجاوز عمرها 7 أيام — عتبة ClamAV نفسه — أو التي لا يُعرف عمرها تخفض الجاهزية (malware_signatures_stale، malware_signatures_unknown)، وتطلق جولة الصيانة تنبيه العمليات documents.malware_signatures مرة في اليوم ما دام ذلك قائمًا. وما تزال عمليات الرفع تُفحص: يقول التنبيه إن الضابط توقف عن التعلّم، لا إنه مطفأ.

تشغيل freshclam مرة واحدة عند الإعداد لا يكفي: تصدر تواقيع جديدة عدة مرات في اليوم، وقاعدة بيانات لا يحدّثها أحد تتجاوز الأيام السبعة في أسبوع. وكان التثبيت الحي قد شغّله مرة واحدة، عند الإعداد، وبلغ عمره 41 يومًا حين قرأه هذا الفحص لأول مرة (2026-11-02). جدوِله:

  • ويندوز: مهمة في جدولة المهام تشغّل freshclam.exe الذي يكتب freshclam.conf الخاص به قاعدة البيانات التي يقرؤها clamscan — تحت %LOCALAPPDATA%\ClamAV للنسخة التي تعدّها الأسئلة الشائعة، وتحت C:\Program Files\ClamAV لنسخة المثبّت نفسه — بحساب المستخدم الذي تعمل به كاظمه، يوميًا على الأقل، مثلًا schtasks /Create /TN "ClamAV freshclam" /SC DAILY /ST 03:30 /TR "<path to freshclam.exe>" /F.
  • لينكس: خدمة clamav-freshclam من التوزيعة (systemctl enable --now clamav-freshclam).
  • Docker: لا شيء يُفعل. تجلب الصورة التواقيع عند بنائها، وتشغّل نقطة دخولها (scripts/docker-entrypoint.sh) البرنامج الخفي freshclam، الذي يفحص 12 مرة في اليوم بالمستخدم الذي تعمل به كاظمه. وfreshclam الذي لا يستطيع البدء يُقال على مخرجات الحاوية، وتبدأ كاظمه على أي حال.

للتحقق: جاهزية المستندات (malware.signatures.age_days وstatus) بعد تشغيل freshclam. تقرأ كاظمه العمر في كل فحص، فيتغير دون إعادة تشغيل.

من دون clamd، يشغّل كل فحص clamscan، الذي يحمّل قاعدة بيانات التواقيع كاملةً أولًا: 6.5-7 ثوانٍ لأي رفع على التثبيت الحي (الجاهزية: malware.per_scan_load: true). لا تستخدم كاظمه clamdscan إلا حين يجيب clamd نبضَه (malware.daemon: true)؛ فالبرنامج الخفي يبقي التواقيع محمّلة (نحو 1 جيجابايت من الذاكرة) ويستغرق الفحص أقل من ثانية بكثير. شغّل clamd خدمةً على الجهاز لتقايض تلك الذاكرة بالانتظار؛ أما clamdscan على PATH بلا برنامج خفي يجيب فلا يُستخدم أبدًا (لا يستطيع الفحص).


يلتقط النسخ الاحتياطي الأصلي (كل 6 ساعات) documents.db أولًا (sqlite3.backup() ← نقطة زمنية) حين تكون البيانات الوصفية والمهام كلتاهما على SQLite، ثم ينسخ المحتوى الذي تشير إليه ويتحقق من بصمات قاعدة البيانات والكتل والبيانات (manifests) المنسوخة. يشتق التحقق المراجع المطلوبة من اللقطة بمعزل عن جرد البصمات؛ والبايتات المطلوبة المفقودة أو الجرد غير الصالح يُفشلان النسخ الاحتياطي. يعدّ الاحتفاظ اللقطات المُتحقَّق منها (تحتفظ المهمة المجدولة بأحدث خمس)، فلا تُزيح محاولة فاشلة نقطة استعادة جيدة. تهبط النسخ الاحتياطية تحت kazma-data/backups/document-store-<ts>-<unique-id>/ مع manifest.json. بالنسبة لمهام SQLite، تحدّد أحداث التحقق الدائمة الأصول المُرقّاة بوصفها مصدر التنفيذ المطلوب. وقد تكون نسخ الحجر الصحي الزائدة قد جُمعت بالفعل. وتعيد الاستعادة التحقق من أصل ثابت متاح حين يحتاج الاستيراد إلى التحقق من جديد.

لا يوجد أمر استعادة لمخزن SQLite: مع إيقاف كاظمه، انسخ محتويات دليل النسخة الاحتياطية إلى جذر تخزين فارغ (documents.storage_root المهيّأ، وافتراضيًا kazma-data/document-store) وابدأ. يُستأنف العمل المنتظر، وتعود قوائم ACL وتاريخ المهام مع المستندات؛ هذا هو التمرين الذي يشغّله tests/test_document_backup_restore_hardening.py، ويكرّره scripts/certify_documents.py لمستند واحد (backup_restore_sqlite).

النسخ الاحتياطي يتبع الفهرس (catalog) (documents/backup.py، installed_document_backends، منذ 2026-10-13): مكان البيانات الوصفية للمستندات هو ما يحدّد المسار، ويُقرأ من الخدمة العاملة أو، في عملية CLI لا خدمة فيها، من الخلفيات المهيّأة.

  • بيانات وصفية SQLite بجوار مهام Postgres (KAZMA_DOCUMENTS_METADATA_BACKEND=sqlite على تثبيت Postgres) تُلتقط كمخزن SQLite، من documents.db. أما ما إذا كان مصدر إصدارٍ مطلوبًا من quarantine أو من originals فتحدّده أحداث المهام، وهي حينئذٍ في Postgres: تُقرأ من الطابور بعد اللقطة (الترقيات تتراكم فقط، فذلك مجموعة شاملة آمنة) وتُحفظ في manifest.json الخاص بالنسخة (promoted_versions، jobs_backend: "postgres") كي يقرأ التحقق اللاحق الشيء نفسه. الطابور نفسه ليس في هذه النسخة؛ إنه في تفريغ Postgres المشترك (pg_shared_*). سابقًا كانت المهمة المجدولة تأخذ هذا الشكل إلى النسخ الاحتياطي المنسّق أدناه لأن المهام كانت على Postgres، فكانت تقرأ فهرسًا غير موجود هناك (أو تنسخ جداول تركها إعداد سابق).
  • بيانات وصفية Postgres بجوار مهام SQLite (بعد رجوع فقط: تعذّر بدء مستودع Postgres الخاص بالطابور) لا تستطيع أي نسخة احتياطية أن تمسكها عند نقطة زمنية واحدة، فتُرفض بالاسم (document_backup_backend_unsupported).
  • تُكتب النسخة الاحتياطية تحت اسم مخفي .…partial ولا يُعاد تسميتها إلى مكانها إلا حين تكتمل؛ والتشغيل الفاشل لا يترك شيئًا يشبه نسخة احتياطية (كان يترك مجلد document-pg-* فارغًا).

لا تحلّ قاعدة بيانات SQLite بديلة (fallback) محلّ فهرس Postgres أبدًا: تأخذ أدوات الترحيل المسار نفسه بحسب الفهرس، انظر الترحيل.

مخزن PostgreSQL (البيانات الوصفية والمهام على Postgres) تنسخه احتياطيًا مهمة الـ 6 ساعات نفسها عبر النسخ الاحتياطي المنسّق (kazma_core.documents.backup_pg)، كنقطة زمنية واحدة: تصدّر معاملة REPEATABLE READ لقطتها، وتقرأ داخلها أيّ الكتل والبيانات (manifests) تشير إليها جداول المستندات، وتثبّتها كي يحتفظ بها جمع القمامة، ويفرّغ pg_dump --snapshot جداول المستندات كما رأتها تلك اللقطة؛ ثم تُنسخ الملفات المُشار إليها وتُحسب بصماتها. تهبط النسخ الاحتياطية تحت kazma-data/backups/document-pg-<ts>-<id>/ مع manifest.json (وقت اللقطة وموضع WAL، وأعداد الصفوف لكل جدول، والبصمات). وقت اللقطة هو نقطة الاستعادة: كل ما يُكتب بعده ليس في تلك النسخة.

نافذة طرفيّة
python scripts/document_pg_backup.py backup --dest <dir> [--storage-root <dir>]
python scripts/document_pg_backup.py verify <backup dir>
python scripts/document_pg_backup.py restore <backup dir> --target-url <database URL> --storage-root <empty dir>

يُعثر على pg_dump/pg_restore كما في نسخ Postgres الاحتياطية الأخرى: على PATH، أو عبر docker exec داخل حاوية قاعدة البيانات (KAZMA_DB_CONTAINER). تحتفظ المهمة المجدولة بأحدث خمس نسخ document-pg-* مُتحقَّق منها؛ وbackup --dest اليدوي لا يحذف شيئًا.

تتحقق الاستعادة من النسخة الاحتياطية أولًا، ولا تدخل إلا قاعدة بيانات لا تحوي أيًّا من جداول المستندات بعد (فارغة، أو لا تحوي إلا جداول كاظمه الأخرى) وجذر تخزين فارغًا؛ وترفض أي شيء آخر. تستعيد في معاملة واحدة تتوقف عند أول خطأ، وتعيد فحص عدد صفوف كل جدول مقابل اللقطة، وتُنهي حيازات المصدر الميت وتعلّم استيراداته غير المنتهية بأنها مستعادة كي يتراجع عنها البدء التالي. ثم وجّه التثبيت إلى المخزن المستعاد وأعد التشغيل: تُستأنف المهام المنتظرة والمقطوعة.

استعادة نسختي Postgres الاحتياطيتين كلتيهما في قاعدة بيانات واحدة (مُتمرَّن عليها: tests/test_document_pg_restore_order.py). تفريغ Postgres المشترك (pg_shared_*.dump، انظر التعافي من الكوارث) يحوي أيضًا جداول المستندات، لكن ليس كما كانت لحظة نسخ ملفات المحتوى. استعد بهذا الترتيب:

  1. التفريغ المشترك (python scripts/pg_backup.py restore ...) في قاعدة البيانات الجديدة: جداول كاظمه الأخرى، وجداول مستندات أقدم مما ينبغي.
  2. النسخة الاحتياطية المنسّقة مع --replace-document-tables (python scripts/document_pg_backup.py restore <backup dir> --target-url <url> --storage-root <empty dir> --replace-document-tables): بعد أن تتحقق النسخة ويُوجَد جذر التخزين فارغًا، تُسقط جداول المستندات الخاصة بالتفريغ في معاملة واحدة وتستعيد جداولها هي.

الترتيبات الأخرى تفشل، كما تُظهر الضوابط السلبية للتمرين: من دون الخيار ترفض الاستعادة المنسّقة قاعدة بيانات فيها جداول مستندات؛ والاستعادة المنسّقة أولًا ثم التفريغ المشترك تعيد صفوف المستندات الأقدم من التفريغ فوقها (--clean)، فيحتفظ مستند استُورد بين النسختين بملفاته ويفقد صفّه في الفهرس.

فهرس المعرفة (مقاطع المكتبات في مخزن الإعدادات، والمتجهات في Chroma) مخزن منفصل وليس في هذه النسخة الاحتياطية؛ تسرد الاستعادة المستندات المفهرسة تحت reindex_required. وكل بدء لعمّال المستندات يقارن بين الاثنين ويعيد نشر كل إصدار يسجّله مخزن المستندات مفهرسًا ولا تحويه مكتبته، من بيان الإصدار نفسه، بصفته مالك المستند. والمكتبة غير الموجودة على هذا التثبيت، أو الإصدار الذي لم يعد الإصدار الحالي للمستند، أو البيان المفقود يُبلَغ عنه في السجل، ولا يُختلق أبدًا. يحدث هذا على أيٍّ من خلفيتي البيانات الوصفية، متى كانت منظومة المعرفة متاحة. لرؤية المقارنة، أو لإزالة مقاطع مستندات لا يسجّلها المخزن مفهرسةً (محذوفة، أو مجهولة، أو أُلغيت فهرستها منذئذٍ)، شغّل مع إيقاف كاظمه (--tenant <id> يقصره على مستأجر واحد):

نافذة طرفيّة
python scripts/document_pg_backup.py index # report only
python scripts/document_pg_backup.py index --apply # re-publish what is missing
python scripts/document_pg_backup.py index --apply --prune # ... and remove what is not recorded

قيس على PostgreSQL 16.15 حقيقي في حاوية بـ 1 CPU و512 MiB على Windows 11:

المخزنالنسخ الاحتياطيالتحققالاستعادةالبدءأول مستند مقروءتصريف المتأخرات
3 مستندات (tests/test_document_pg_backup_restore.py)0.19 ث—0.24 ث (قاعدة البيانات)—1.05 ث بعد البدءكل المهام جاهزة بعد 3.8 ث من بدء الاستعادة
1,000 مستند، 62.5 MiB (tests/test_document_pg_restore_drill_scale.py)1.14 ث (66.8 MB)0.48 ث1.39 ث1.05 ث2.45 ث بعد بدء الاستعادة20 مهمة في 22.5 ث (نحو 0.9 في الثانية)
10,000 مستند، 625 MiB، ملأها 6 كتّاب (حاوية بـ 2 CPU و3 GiB)9.64 ث (664 MB)5.17 ث10.67 ث1.08 ث11.77 ث بعد بدء الاستعادة20 مهمة في 23.1 ث

ينمو النسخ الاحتياطي والتحقق والاستعادة مع المخزن (خطيًا تقريبًا من 1,000 إلى 10,000 مستند)؛ أما بدء الخدمة المستعادة فلا. وأظهر ملء المخزن بعدة كتّاب في آن واحد أيضًا أن عمليات بدء الخدمة المتوازية على قاعدة بيانات جديدة كانت تقع في جمود (deadlock) على إعداد المخطط؛ وصار إعداد المخطط الآن لا يأخذ قفل جدول لا يحتاجه (kazma_core/db/pg_schema.py).

هذه أرقام هذه الآلة، لا زمن استعادة (RTO) لنشر: نقطة الاستعادة هي لقطة آخر نسخة احتياطية ناجحة (كل 6 ساعات افتراضيًا)، وتصريف متأخرات مستعادة يستغرق ما استغرقته معالجتها. شغّل التمرين على حجم مخزنك وعتادك: اضبط KAZMA_TEST_RESTORE_DRILL_DOCUMENTS (وKAZMA_TEST_RESTORE_DRILL_KIB، وKAZMA_TEST_RESTORE_DRILL_WRITERS، وKAZMA_DRILL_REPORT) مع قاعدة بيانات اختبار PostgreSQL حقيقية.

يُتمرَّن على حيازة الاستيراد بالمالك المتقادم في حاوية أخرى حقيقية (tests/test_document_intake_lease_containers.py، اضبط KAZMA_TEST_CONTAINER_LEASE_DRILL_IMAGE على صورة تحوي اعتماديات كاظمه): مجمّدًا بـ docker pause أو مقطوعًا عن الشبكة، يُستولى عليه بمجرد انقضاء حيازته بحسب ساعة قاعدة البيانات ويُرفض حين يعود؛ ولا يُنشر شيء مما كتبه. يبدأ التمرين شبكته وPostgreSQL وحاوياته بنفسه ويزيلها.


ينقل kazma migrate export|verify|import مخزن المستندات كما يتطلب فهرسه (منذ 2026-10-13، القاعدة نفسها التي للنسخ الاحتياطي؛ تقرأ CLI الخلفيات المهيّأة، إذ لا خدمة عاملة لديها):

  • فهرس SQLite: documents.db (عند وجوده) + شجرة العنونة بالمحتوى + البيانات (manifests). لا تحتوي documents.db على مسارات مضمّنة، فلا تحتاج إعادة كتابة. وبجوار طابور Postgres تُقرأ الترقيات من الطابور، وينتقل الطابور في data/postgres.dump.
  • فهرس Postgres: ينتقل الفهرس في data/postgres.dump، ويُؤخذ أولًا؛ ثم ينسخ المُصدِّر كل ملف محتوى وبيان يشير إليه ذلك الفهرس (مجموعة شاملة لما في التفريغ: الكتل ثابتة وتُكتب قبل صفوفها). كانت الحزمة لا تحمل أيًّا منها: كان المُصدِّر يقرأ documents.db المحلية الفارغة.
  • فهرس Postgres بجوار طابور SQLite (رجوع): يُرفض بالاسم.

يضع المستورد الملفات في جذر مخزن مستندات الهدف بعد المخازن الأخرى، ولا ينصّب documents.db إلا حين يكون فهرس الهدف على SQLite. والملف المُشار إليه المفقود عند التصدير يعلّم الحزمة بأنها ناقصة ويرفضها verify. ومخزن المستندات الذي يفشل في الاستعادة عند الاستيراد يُفشل الاستيراد (ok: false، والخطأ في errors، والملفات المرحلية محفوظة)؛ وكان سابقًا تحذيرًا بجوار استيراد ناجح. انظر الترحيل.


المكوّنتعدد نسخ؟كيف
طابور المهامنعم (Postgres)أي تثبيت Postgres (KAZMA_DB_BACKEND=postgres أو KAZMA_DATABASE_URL بصيغة postgresql://) + jobs_pg.py؛ التجاوز بـ KAZMA_DOCUMENTS_JOBS_BACKEND=sqlite
قبول الاستيرادمع طابور المهامالحجوزات والحيازات وسياج الاستيلاء تعيش في قاعدة بيانات المهام (documents/admission.py)
البيانات الوصفيةنعم (Postgres)KAZMA_DOCUMENTS_METADATA_BACKEND=postgres، أو auto/غير مضبوط على تثبيت Postgres (‏auto يتبع طابور المهام)؛ ‏repository_pg.py
البيانات الوصفيةلا (SQLite)تثبيت SQLite، أو KAZMA_DOCUMENTS_METADATA_BACKEND=sqlite: نسخة تطبيق واحدة
الكتل (blobs)وحدة تخزين مشتركةيجب أن ترى كل النسخ نفس شجرة documents.storage_root
خانات المخزن المؤقت للرفعلكل عمليةdocuments.capacity.max_concurrent_uploads يعدّ عمليات رفع عملية واحدة
جمع القمامةواعٍ بالخلفيةيوفّر مستودع البيانات الوصفية المراجع الحية المرجعية على SQLite وPostgres

هذا ما تدعمه الخلفيات. لم يُؤهَّل أي نشر مستندات متعدد النسخ: يُبلغ scripts/certify_documents.py عن postgres_multi_replica وmetadata_multi_replica بأنها NOT RUN، وبقية كاظمه تعمل كبيئة تشغيل واحدة (انظر تعدد النسخ وSaaS).

اقرأ دائمًا GET /api/documents/ops/readiness:

{
"status": "ready|degraded|disabled",
"jobs_backend": "postgres|sqlite",
"jobs_multi_replica": true,
"metadata_backend": "postgres|sqlite",
"metadata_multi_replica": true,
"multi_replica": true,
"degraded_reasons": [],
"policy": { "mode": "live", "restart_required": false, "pending_restart": {} },
"malware": { "available": true, "scanner": "clamscan", "daemon": false, "per_scan_load": true,
"signatures": { "built": "2026-11-02T03:27:06", "age_days": 0.4, "stale": false },
"mode": "auto", "fail_closed": false },
"worker_isolation": { "mode": "process", "ready": true, "hostile_document_profile": "unqualified" },
"stranded_intake_reservations": 0,
"accepting_durable_writes": true
}

تكون status بقيمة degraded حين لا تكون خلفية مهيّأة هي التي تخدم (jobs_postgres_unavailable_fell_back_to_sqlite، metadata_postgres_unavailable_fell_back_to_sqlite) أو حين تضيف الخدمة سببًا من عندها: document_worker_isolation_unavailable، وdocument_worker_image_outdated، وdocument_intake_reservations_stranded، وmalware_scanner_unavailable. وتكون disabled (مع underlying_status) حين تُطفأ الكتابات الدائمة (durable_writes_disabled). والخلفية المختارة في .env — بيانات وصفية SQLite بجوار مهام Postgres، وهو الشكل الشائع للخادم الواحد — حالة ready مع metadata_multi_replica: false؛ وmulti_replica لا يكون true إلا حين يكون كلٌّ من المهام والبيانات الوصفية كذلك. (ذلك الشكل ليس له نسخ احتياطي منسّق للمستندات: انظر النسخ الاحتياطي.)


الشهادة واختبار التحمّل (soak)

Section titled “الشهادة واختبار التحمّل (soak)”
نافذة طرفيّة
python scripts/certify_documents.py
python scripts/certify_documents.py --soak --soak-iterations 100
python scripts/certify_documents.py --output report.json

خيار --soak القديم يختار فحص دخان ممتدًا ومحدودًا. ولا يُثبت تحمّلًا لعدة أيام: تلك البوابة تبقى NOT RUN. يسمّي التقرير ما هو مهيّأ الآن — نمط عزل العامل (process هو CONDITIONAL ولا يُبلَغ عنه أبدًا على أنه محصور؛ وcontainer لا ينجح إلا حين يكون الوسيط جاهزًا، ومُصدَّقًا عليه، ويشغّل كود هذا البناء)، وصورة العامل، وخلفيتي البيانات الوصفية/المهام — وفحوصه تشغيلات حقيقية: المجموعة العدائية، وفحص دخان أداء محدود، ومجموعة دقة الاستخراج مقابل أهداف الإصدار (extraction_fidelity)، ونسخ احتياطي وتحقق واستعادة على SQLite لمستند جاهز واحد (backup_restore_sqlite). يُبلَغ عن تخصيصات Python للعملية الأم وعن RSS الأصلي المأخوذ بالعيّنات لشجرة العمليات؛ وأخذ العيّنات ليس قياسًا صارمًا للذروة. لا تؤهّل CLI البيانات الوصفية متعددة النسخ، ولا اختبار تحمّل للتعافي تحت حمل مختلط لمدة 48–72 ساعة، ولا مراجعة أمنية مستقلة.

لاختبار التحمّل نفسه أداة تشغيل، scripts/document_soak.py: خدمة استيراد حقيقية تحت مزيج رفع ثابت (مجموعة الدقة ونصوص صغيرة)، وإعادة تشغيل للخدمة كل --restart-every ثانية، واستيراد يُقتل في منتصف النشر كل --crash-every ثانية، مع الإنتاجية، وp50/p95/p99 من الاستيراد إلى الجاهزية، وRSS لشجرة العمليات، وما يبقى منتظرًا أو في الملفات المؤقتة، في --report الخاص بها. تعمل على مخازن SQLite خاصة ما لم يُعطَ --use-env، وتفشل حين لا تنتهي مهمة، أو يبقى حجز منتظرًا، أو تتجاوز الملفات المؤقتة عمر التشغيل. نجح تشغيل مدته 48 ساعة في 2026-10-19 (الإيداع de461eae، والملف الافتراضي process، ومخازن SQLite خاصة، ومحطة عمل Windows واحدة): كانت المستندات المرسلة كلها، وعددها 12,865، جاهزة، مع 190 إعادة تشغيل للخدمة، و143 استيرادًا قُتل في منتصف النشر، و0 أخطاء، ولا حجز منتظر، ولا ملفات مؤقتة متبقية. جرى بمعدل نحو 4.5 مستندات في الدقيقة: من الاستيراد إلى الجاهزية p50 ‏13.2 s، وp95 ‏14.1 s، وp99 ‏24.3 s، وRSS لشجرة العمليات 1,034 MiB على الأكثر. ولم يُشغَّل على الملف container ولا على مخازن Postgres.

نافذة طرفيّة
python scripts/document_soak.py --duration 172800 --report soak.json
نافذة طرفيّة
python scripts/document_fidelity.py # measure and compare to the floors
python scripts/document_fidelity.py --lock # write today's numbers as the floors

مجموعة ذات إصدارات (مستندات إنجليزية وعربية وثنائية اللغة تعرضها محرّكات DOCX وPDF وHTML الخاصة بكاظمه، إضافةً إلى OCR لصفحة محوّلة إلى صورة نقطية) يحلّلها المُحلِّل الحقيقي وتُقاس: استرجاع الكلمات والمقاطع (يجب أن تعود كلمات المقطع معًا)، وترتيب القراءة، والأرقام الدقيقة، وخلايا الجداول، وادعاءات الصفحات الصادقة، ومحاذاة الفقرات، وCER/WER لـ OCR، و(منذ الإصدار 5) علامات الترقيم: علامة قُرئت خطأً أو سقطت أو أُضيفت بعد كلمة. يضيف الإصدار 2 مستندات طويلة (إنجليزية وعربية، صفحات كثيرة)، وعمودين يرتّبهما منتِج آخر (MuPDF)، وجداول عريضة بخلايا ملتفّة، ومسحًا ضوئيًا متدهورًا (مُدار، ومموّه، ومنقّط، ومضغوط بـ JPEG)؛ ويضيف الإصدار 3 صفحة OCR فيها جدول مسطّر، بالإنجليزية والعربية. يعلن tests/fixtures/documents/fidelity_thresholds.json الحدود الدنيا وأهداف الإصدار؛ والحالات التي تبلغ الأهداف مدرجة بوصفها qualified. في 2026-10-13 بلغتها 16 من 19 حالة. غير مؤهّلة: OCR العربي، بجدول أو بدونه (يخطئ Tesseract ‏ara قراءة الأرقام العربية الهندية ويتخطى سطرين يسقطهما تحليل تخطيطه؛ CER 0.23)، والأرقام المقروءة بـ OCR من المسح المتدهور (3.75 تُقرأ 3:75). ومنذ 2026-10-15 تُقاس حالات OCR لكل محرّك على حدة: مع المحرّك العصبي تبلغ حالات OCR الخمس كلها الأهداف (19 من 19)؛ ومع Tesseract ما زالت الثلاث المذكورة أعلاه لا تبلغها. المجموعة مولَّدة: الكتابة اليدوية والنماذج والجداول ذات الخلايا المدموجة غير ممثَّلة.

المسوح الحقيقية تُقاس اختياريًا (tests/test_document_real_scans.py): يسمّي KAZMA_TEST_REAL_SCANS_MANIFEST بيانًا يُحفظ خارج المستودع — مسار كل مسح وSHA-256 الخاص به، والصفحات المراد قياسها، والأرقام والرموز والمصطلحات المفردة التي تحويها كل صفحة (لا نصّها أبدًا)، وعمليات بحث يجب أن تجد صفحتها، والحدود الدنيا التي بلغها هذا البناء. يمرّ المسح عبر الاستيراد الحقيقي وOCR والفهرسة والبحث. ومن دون البيان يُتخطى الاختبار.

اختبارات Pytest: ‏tests/test_document_certification_phase10.py (البنية، والانهيار، وإمكانية الوصول a11y، والإطلاق التدريجي). تاريخ التقارير: docs/audits/AUDIT_DOCUMENT_CERTIFICATION.md.