دليل أسطرلاب English GitHub

التطوير

تشغيل أسطرلاب من المصدر، وسكربتات البوابات، وأدوات لقطات الشاشة، وكيف تساهم بتغيير.


وضع التطوير

npm run dev

يشغّل هذا الأمر شيئين جنبًا إلى جنب: خادم الواجهة البرمجية، وVite، وهي الأداة التي تقدّم العميل وتعيد تحميله في المتصفح لحظةَ تحفظ ملفًا.

المنفذما هومتى
6801خادم Hono (الواجهة البرمجية + العميل المبني)npm start / دائمًا
5801خادم Vite للتطوير (يمرّر /api إلى 6801)npm run dev فقط

في وضع التطوير تفتح المنفذ 5801؛ وتُمرَّر الطلبات إلى /api إلى الخادم على 6801. ويغيّر PORT منفذ الخادم.

السكربتات

السكربتما يفعله
npm run devخادم الواجهة البرمجية (بخيار --watch في Node نفسه) وVite جنبًا إلى جنب
npm run buildيبني العميل في dist/. ولا يحتاج الخادم إلى بناء، فـNode يشغّل TypeScript مباشرة
npm startيبني ثم يقدّم
npm run serveيقدّم من غير إعادة بناء
node scripts/rebrand.mjs --name … --icon …يطبع اسم منتجك وأيقونتك على بناء سطح المكتب قبل npm --prefix desktop run dist (تطبيق سطح المكتب)
npm run hash-passwordيطلب كلمة مرور (من غير عرضها، أو من stdin) ويطبع تجزئة argon2id لوضعها في ADMIN_PASSWORD_HASH
npm run typechecktsc --noEmit: بوابة TypeScript الصارمة
npm testطقم الاختبارات، node --test tests/*.test.ts: المنطق الخالص تحت shared/ وserver/ وclient/ من غير متصفح. وهو بوابة الإصدار، ويشغّل الكود نفسه الذي تشغّله بعض البوابات أدناه (check-keymap وcheck-docs) من باب ثانٍ
npm run build-docsيبني هذا الدليل، باللغتين، في docs/site/
npm run check-docsكل رابط ومرساة وصورة ومسار إعدادات في هذا الدليل يصل إلى مقصده، باللغتين (أدناه)
npm run gen-iconsيعيد رسم طقم علامات المجلدات من كتالوجه؛ ويفشل npm run check-icons حين يكون الرسم قديمًا
npm run check-desktopفحوص غلاف سطح المكتب نفسه، ثم tsc الخاص به
npm run check-windows-layoutهيكل سطح المكتب عند خمسة عروض نافذة × ثلاث نسب بكسل × أوضاع مؤشّره، وأيّ هيكل يأخذه كل عرض (أدناه)

أين يسكن الكود

العميل في client/ (React؛ هيكل سطح المكتب هو App.tsx، وهيكل الهاتف في client/phone/)، والخادم في server/ (Node وHono)، والمنطق الذي يستعمله الاثنان في shared/، وهيكل أندرويد في mobile/، وغلاف سطح المكتب في electron/ وdesktop/، والبوابات في scripts/، والاختبارات في tests/. والوحدات التي كبرت أكثر من غيرها قُسّمت إلى أجزاء على طول مفاصلها هي، واحتفظت كل واحدة باسمها وبكل ما تصدّره، فمن يستورد منها يقرأ ما كان يقرؤه دائمًا (ويُلزم tests/splits.test.ts كل عائلة بذلك):

الوحدةأجزاؤها
server/indexer.ts: مخزن الفهرس، وبناؤه، وطابور الأحداثserver/indexer/: language وresolve وfolders وpublish وposts وqueries
server/api.ts: الوسائط، وحارس الدخول، والملاحظات والمجلدات، والبحث، ونقاط التركيبserver/*Routes.ts، موجّه لكل مجموعة (المهملات، والوسوم، والاستبدال، والملفات، والتعليقات، والمدارات، والإعدادات، والمزامنة، والنسخ، وتيار الأحداث، وإعادة التسمية)، مركّب حيث كانت مساراته؛ وserver/requestBody.ts
client/state.ts: المخزنclient/state/: الأنواع، وما يُكتب في المستند، والتخزين المحلي، والمساعدات، ومرآة السمة، وخمس شرائح (الحقول، والجلسة، ومساحة العمل، والتفضيلات، والملاحظات)
client/components/Sidebar.tsxclient/components/tree/ (الفتح والطيّ، والأيقونات، والصف، ومؤشر لوحة المفاتيح) وTagShelf.tsx
client/components/CommandPalette.tsxclient/components/palette/commands.ts: جدول الأوامر الذي يشغّله الهيكلان
client/components/GraphView.tsxclient/graph/sim.ts: المحاكاة واللوحة
client/books/BookReader.tsxReaderPanels.tsx (الطبقات) وpdfHighlight.ts (نتيجة البحث)
client/styles/app.cssreset.css وtree.css وeditor.css وpublish.css مربوطة قبله، وgrips.css بعده، في client/index.html، حيث الترتيب هو التتالي
client/i18n.ts: t() والمحمّلclient/i18n/en.ts (قائمة المفاتيح) وclient/i18n/ar.ts، جزء لكل لغة

البوابات

البوابة سكربت يفحص وعدًا واحدًا بعينه يقطعه المنتج، ويخرج بخطأ حين يُخلَف الوعد. وكل بوابة أدناه تخرج برمز غير صفري عند الفشل. والبوابات التي لا تحتاج إلى متصفح تعمل من غير أي تجهيز. أما بوابات المتصفح فتحتاج إلى نسخة من التطبيق تعمل، ومعها npm i -D playwright، ثم إما npx playwright install chromium وإما متصفح النظام عبر CHROMIUM=/usr/bin/chromium. والتي تسجّل الدخول تأخذ ASTROLABE_PASSWORD (والوضع المحلي المفتوح لا يحتاج إلى كلمة مرور).

npm run check-i18n: القاموس

كل نص يستطيع المستخدم أن يراه يأتي من قاموس واحد في ملفين: client/i18n/en.ts، وهو قائمة المفاتيح، وclient/i18n/ar.ts، المُلزَم بها بالنوع، فالمفتاح الموجود في أحدهما دون الآخر لا يُترجَم برمجيًّا. والصفحة لا تنزّل إلا اللغة التي تتكلمها. وتفشل هذه البوابة إن كان أي مفتاح t() ناقصًا، أو غير مترجم، أو ميتًا (أي لا يستعمله أحد)، أو إن اختلف الجانبان الإنجليزي والعربي لمدخل واحد في {placeholders} التي يحملانها. وتفشل أيضًا عند وجود نص إنجليزي مكتوب مباشرة في JSX وفي الكود الذي يبني عناصر DOM بيده. ويُحسب «الميت» من مواضع الاستدعاء وحدها: فملف القاموس مستثنى من فحص الاستخدام، لأن المفتاح الذي تصادف أن قيمته الإنجليزية هي اسمه نفسه (read: { en: "read" }) كان سيطابق داخل تعريفه هو ويبلّغ عن نفسه مستخدَمًا.

وهي تقرأ شجرة TypeScript النحوية لا الأسطر (scripts/i18nScan.mjs). فالفحص السطري الذي حلّت محلّه لم يكن يرى كلمة مفردة، ولا تعبيرًا في سمة (title={open ? "Hide" : "Show"})، ولا قالبًا نصيًّا، ولا نص JSX موزّعًا على أسطر، ولا aria-description ولا aria-valuetext، ولا نصًّا ابنًا بين قوسين؛ ويشغّل tests/i18nScan.test.ts الفحصَ المتقاعد بجانب الجديد على كل شكل من هذه ويُظهر أنه يفوته. والنص الحرفي الذي ليس نسخةً للقارئ — حقل مصيدة لا تراه إلا الروبوتات، أو اسم مجلد افتراضي حرفي — يقول "not copy" على سطره أو فوقه مباشرة، ويقول لماذا. وتقرأ البوابة أيضًا هيكل أندرويد، mobile/src، الذي يحفظ قاموسه الخاص باللغتين لأن شاشاته تتكلم قبل أن يُحمَّل العميل: كل قيمة موجودة باللغتين، والعربية بالعربية، والمعاملات المُدرجة نفسها، وكل مفتاح مستعمل، ولا إنجليزية في دوالّ بنائه ولا في أجسام الأخطاء التي يجيب بها عامل الخدمة فيه.

npm run check-names: بوابة الأسماء

أُعيدت تسمية ميزتين في 3.15 ثم في 3.16: الروتين اليومي صار السِّجِلّ، والمدارات صارت التكرار المتباعد. وإعادة التسمية التي تترك رسالة واحدة أو تلميحًا واحدًا أو عنوانًا واحدًا يقول الكلمة القديمة أسوأ من ألّا تكون؛ ولذلك تبحث هذه البوابة في كل سطح يراه القارئ — كل قيمة إنجليزية وعربية في client/i18n/en.ts وclient/i18n/ar.ts وفي client/orbits/copy.ts، وكل صفحة من هذا الدليل باللغتين، وREADME، والخزانة الأولية، وعرض «ما الجديد»، وعناوين CONTRACTS.md وكل ملف في contracts/ — عن الكلمات التي لم يعد يجوز أن تظهر فيها (كلمة الروتين، والاسم الذي بُنيت صفحة المذاكرة تحته، وكلمة «بطاقات تعليمية» وهي «بطاقات» الآن)، وفي client/ وserver/ عن عناوين الصفحات القديمة، التي لا يجوز أن تبقى إلا مصادرَ لإعادة توجيه. والسطر الذي عليه أن يروي التاريخ (أي السياجات الأقدم ما زالت تعمل، وماذا كان اسم الإصدار وقتها) يحمل كلمة lineage في تعليق على السطر نفسه — <!-- lineage --> في Markdown و// lineage في العرض — فيُتخطى؛ أما قيمة في القاموس فلا تنال هذا الاستثناء أبدًا. وتُطبع كل إصابة على هيئة file:line، ويخرج السكربت برمز غير صفري.

npm run check-contrast: بوابة إمكانية الوصول

تُلزم هذه البوابة كل سمة من السمات الست والأربعين في client/styles/tokens.css بقواعد التباين في WCAG على رموز النص الخمسة: نص المتن، والعناوين، والنص الثانوي، مقابل الخلفيات الثلاث كلها (--bg، والأسطح المرتفعة، وخلفية التحويم التي تجلس عليها حبوب الوسوم)، ولون التمييز مقابل الصفحة، و--text-faint عند حدّ 3:1 لغير النص على الخلفيتين اللتين يُسمح له بالرسم عليهما. ويُقرأ زوج التمييز نصًّا مرتين (الروابط الويكية وحبوب الوسوم في النثر، وحبّة الوضع المضاءة، وهي اللونان نفسهما مقلوبين). شغّلها بعد أي مساس برموز السمات.

وتعيش الصيغ والحدود الدنيا في shared/contrast.ts، وهو ما يستورده باني السمات المخصصة أيضًا؛ تنفيذ واحد، حتى لا يبارك الباني يومًا سمةً ترفضها البوابة.

وتفحص أيضًا لوحات ألوان النص (shared/textColors.ts وclient/styles/textcolor.css). وهذه موجودة في طبقتين لسبب حسابي: فمقابل #050508 في سمة void يحتاج اللون إلى إضاءة نسبية ≥ 0.186، ومقابل #ffffff في سمة solar يحتاج إلى ≤ 0.183، ولذلك لا يوجد لون واحد يجتاز AA على كل السمات. فالطبقة الواعية بالسمة (var(--vc-*)، وهي الافتراضية) تحمل قيمة واحدة لكل مجموعة سمات وتُلزم بـ4.5:1 مقابل كل خلفية في مجموعتها. والطبقة ذات الحبر الثابت تحمل قيمة سداسية واحدة لكل السمات وتُلزم بـ3:1، وهو حدّ غير النص في WCAG 1.4.11، وأقصى ما يستطيع لون ثابت أن يعد به. وتطبع البوابة الاثنتين، وتفحص قيم ورقة الأنماط مقابل قيم الوحدة.

npm run check-sections: بوابة جراحة الأقسام

لا متصفح ولا خادم. فسحب عنوان في المخطط يعيد كتابة الملاحظة: كتلة من الأسطر تغادر مكانًا وتصل إلى آخر، مع إعادة مستويات عناوين الشجرة الفرعية المنقولة. وهذه أشدّ عملية تدميرًا في المنتج لا تحمل اسم «حذف». تعمل بإيماءة بلا مفتاح، وهي على بعد زلّة 4 بكسل من أن تحدث بالخطأ، والقارئ ينظر إلى مخطط من أربعين صفًّا لا إلى 1,200 سطر يُعاد ترتيبها، فالفقرة الواحدة التي تسقط ستبقى خفيّة حتى اليوم الذي تُحتاج فيه.

تولّد البوابة آلاف الوثائق من الأشكال التي تكسر التنفيذات الساذجة (مقدمة YAML، وأسيجة كود تحوي أسطرًا تبدأ بـ### ، وعناوين تقفز مستويات، وأقسام فارغة، وقسم في آخر الملف، وCRLF، وغياب السطر الجديد الأخير)، وتؤكد أن إعادة الترتيب تبديل خالص: يجوز لها أن تغيّر ترتيب أسطر الملاحظة وعمق عناوين الشجرة الفرعية المنقولة، وأن تضيف سطرًا فارغًا عند موضع الوصل؛ ولا يجوز لها أبدًا أن تفقد سطرًا أو تكرّره. وتؤكد أيضًا أن القسم لا يمكن إسقاطه داخل نفسه، وأن النقل بمسافة صفر لا يغيّر شيئًا، وأن نصفَي الاستخراج يغطّيان الأصل بالضبط. وSEED=… يعيد تشغيل فشل بعينه، وROUNDS=… يضبط حجم العيّنة.

npm run check-caret: بوابة النقر إلى المؤشر

تستبدل المعاينة الحية مصدرَ ماركداون بصناديق معروضة تختلف عنه في العرض وفي الطول (ثمانية عشر محرفًا من $7.7\ \text{km/s}$ تقف تحت سبعة رموز من KaTeX)، فأي ربط بين موضع المؤشر والوثيقة يفكّر بالهندسة بدل أن يفكّر بـDOM ينحرف بمقدار ذلك الفرق بالضبط.

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

وهي موجودة لأن ذلك السؤال الواحد انكسر هنا بأربع طرق مختلفة (موضع النقر، ومعاينات التحويم، والتنقّل بالنقر مع مفتاح التعديل، وتحديد النص)، والسبب المشترك دائمًا بكسلٌ لا تراه خريطة الارتفاعات في المحرر: لا يجوز لأي شيء داخل .cm-content أن يحمل هامش CSS رأسيًّا. فـCodeMirror يقيس كل سطر وكل عنصر كتلة بصندوق حدوده، فتُحسب الحشوة والحدود ولا تُحسب الهوامش؛ فضع الفراغ في حشوة غلاف، أو في حدّ شفّاف مع background-clip: padding-box، ولا تضعه في هامش أبدًا. وتعيد البوابة لغة النسخة وتحذف ملاحظتها التجريبية كيفما انتهى التشغيل.

npm run check-french: بوابة التصحيح التلقائي

يثبت tests/french.test.ts الجدولَ (لا مصدر فيه كلمة فرنسية حقيقية، ولا تكرار، وكل هدف لا يختلف عن مصدره إلا بالحركات) وكاشفَ السطر. أما ما لا يثبته إلا المتصفح فهو نصف المحرر: التصحيح معاملة ثانية تُرسَل في مهمة دقيقة بعد المعاملة التي كتبت المسافة، ويجب أن تستقر خطوةَ تراجع مستقلة والمسافة باقية في مكانها. فتكتب البوابة ملاحظة، وتكتب فيها بلوحة مفاتيح حقيقية، وتفحص كل وعد يقطعه الدليل: tres ← très ، وcoeur ← cœur ، وEnter وقوسَ إغلاق مُتخطًّى حدًّا للكلمة، وسطرًا يُصحَّح كله حين يصير فرنسيًا، وسطرًا إنجليزيًا فيه كلمة فرنسية واحدة (وUN بحروف كبيرة، وسطرًا إسبانيًا) يُترك كما هو، وسياج شيفرة لا يُمسّ أبدًا، وCtrl Z واحدة تعيد tres والكلمةَ نفسها لا تُصحَّح ثانية، ومفتاح الجهاز مطفأً ومشغّلًا، وlang="fr" على السطر الفرنسي ولا شيء على الإنجليزي، والمسافة الضيقة غير الفاصلة قبل ?، و... ← … على السطر الفرنسي وحده، وتصحيحًا في وضع الإدراج في Vim. تحتاج CHROMIUM، ومع نسخة محمية بكلمة سر ASTROLABE_PASSWORD؛ وتحذف ملاحظتها التجريبية كيفما انتهى التشغيل.

npm run check-layouts: بوابة تخطيط لوحة المفاتيح

KeyboardEvent.key هو المحرف الذي أنتجه تخطيط لوحة المفاتيح. وكان التطبيق يقارنه بالحروف اللاتينية، فعلى لوحة مفاتيح عربية، حيث يبلّغ المفتاح المكتوب عليه P عن ح، كان كل اختصار عام في المنتج ميتًا، في تطبيق يأتي بترجمة عربية كاملة ويعكس واجهته كلها من أجلها. ولم يلتقط ذلك أي اختبار، لأن كل اختبار كان يكتب حروفًا لاتينية.

فهذه البوابة لا تفعل ذلك. تقود التطبيق الحقيقي عبر بروتوكول DevTools (Input.dispatchKeyEvent، وهو الطريقة الوحيدة لضبط key وcode وkeyCode كلٍّ على حدة؛ فلوحة مفاتيح Playwright نفسها ترسل دائمًا key الأمريكي لكل code) بضغطات المفاتيح التي ترسلها فعلًا لوحات العربية 101، وЙЦУКЕН، واليونانية، والعبرية، وAZERTY، وDvorak، وQWERTY الأمريكية، وتؤكد أن لوحة الأوامر، والمخطط، وورقة الاختصارات، والصفاء، وعرض القراءة، والبحث، ومفاتيح اللوحات، والغامق، والمشطوب، كلها ما زالت تعمل، بما فيها Ctrl/Cmd K في قشرة المدونة من غير تسجيل دخول، وهو الاختصار الوحيد عند القارئ المجهول. 72 فحصًا؛ نجح 46 منها قبل الإصلاح.

ونصفها الثاني هو ما يمنع الإصلاح من الإفراط في التصحيح: فعلى Dvorak المفتاح الذي يكتب b هو KeyN الفيزيائي، فيجب أن يفعل Ctrl Alt على KeyB الفيزيائي، الذي يكتب x هناك، لا شيء. التخطيط أولًا، والموضع الفيزيائي احتياطًا فقط. ويشغّل tests/shortcuts.test.ts المجموعة نفسها على المحلّل من غير متصفح إطلاقًا، ويحمل الحالات التي لا يستطيع متصفح تقديمها (فـChromium يسطّح لام ألف العربية ذات النقطتين الرمزيتين إلى key فارغ). والاختصار الذي يُضاف إلى ورقة الاختصارات ولا يُضاف إلى ذلك الملف اختصارٌ لم يُختبر على أي لوحة مفاتيح غير لاتينية على وجه الأرض.

وnode scripts/shoot-layouts.mjs هو الصورة المرافقة: ورقة Ctrl/Cmd / مع خريطة التخطيط مثبّتة على العربية وعلى الروسية، وهكذا تُراجع أغطية المفاتيح المشروحة.

npm run check-windows-layout: بوابة النافذة

كل بوابات المتصفح الأخرى هنا تنظر إلى التطبيق عند عرض أو عرضين مريحين. والتطبيق لا يُستعمل عند عروض مريحة. فلوحة حاسوب محمول على ويندوز عرضها 1366 بكسل فيزيائيًا، أي 1093 بكسل CSS عند تحجيم 125% و904 عند 150%؛ ونصفها تحت Win+← هو 683. أربع جولات إصدار من «تغيير حجم اللوحات والنوافذ على ويندوز أخرق وغريب» كانت أربعة عيوب منفصلة لم تستطع أي بوابة رؤيتها، لأن كل واحد منها دالةٌ في مساحة العرض والمؤشّر معًا، ولأن الجهاز الوحيد الذي لا يملكه أحد منا هو الجهاز الذي وقعت عليه كلها.

فهذه البوابة سُلّم لا لقطة شاشة. تقود التطبيق المبنيّ عند 1366 و1280 و1024 و900 و720 بكسل CSS — عروض هيكل سطح المكتب نفسه — كلٌّ منها عند نسبة بكسل الجهاز 1 و1.25 و1.5، في الاتجاهين، مع عرضَي اللوحتين فارغين ومزروعين بزوج يستطيع القارئ فعلًا أن يسحبهما إليه ({560, 560})، تحت أوضاع المؤشّر الثلاثة التي يبلّغ عنها Chromium حقًّا على ويندوز:

الوضعما هوما يجب أن يعطيه
mouseبرج مكتبي أو حاسوب محمول عاديلوحات مرسّاة ومقابض عند كل عرض فوق 700
slateلوح صلب: لمس، ومستشعر دوران، وبتّة اللوح في ACPI. والفأرة الموصولة لا تغيّر جواب Chromiumهيكل الهاتف، بكل عرض
touchlaptopمؤشّر دقيق وإصبع معًالوحات مرسّاة، ومقابض، وصفوف 44 بكسل

والأوضاع إعدادات Blink على عملية المتصفح (--blink-settings=availablePointerTypes=…)، وهي ما يسلّمه pointer_device_win.cc نفسه إلى المُصيّر، لا محاكاة الوسائط في DevTools التي يُسقطها setViewportSize بصمت في منتصف السُّلّم.

أيّ هيكل. تمشي درجة أخيرة، whichShell، بكل وضع عبر العروض (و700 و600 أيضًا)، وتؤكد هيكل الهاتف تحت 700 وعلى اللوح بكل عرض، وهيكل سطح المكتب — كما هو بلا تغيير — في كل ما عدا ذلك. (وحتى 3.27.0 كان السُّلّم يجري في تخطيط الهاتف الكلاسيكي ليقيس خلايا الدرج فيه؛ وذهب الدرج معه، فكل خانة يُركَّب فيها هيكل سطح المكتب مرسّاة، وتقول ذلك.)

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

وثلاث درجات بعد السُّلّم تراقب الإطارات التي لا يراها السُّلّم، لأنه ينتظر 300 مللي ثانية بعد كل تغيير حجم، والانتقال الذي مدّته 0.18 ثانية يكون قد انتهى حينئذ. وهي تسأل عمّا يراه القارئ فعلًا بينما تُسحب النافذة وبينما تُطوى اللوحة: أن عمود الملاحظة ما زال له 320 بكسل بعد إطار واحد من تغيير الحجم (عند نسبتين وفي الاتجاهين)، وأن طيّ القارئ بيده عبر Ctrl/Cmd Alt B ما زال في منتصف حركته بعد 60 مللي ثانية، عند عرضين. وقد فشلت كلها في وقت ما على الرأية نفسها — الصنف الذي يقول «هذا عرضٌ ليس من صنع اليد» — إذ كانت مرفوعةً حيث ينبغي أن تكون منزَّلة، أو منزَّلةً حيث ينبغي أن تكون مرفوعة.

تحتاج نسخةً تعمل، وCHROMIUM، وكلمة سر النسخة وسيطًا ثانيًا.

npm run check-windows-layout -- http://127.0.0.1:8177 <password>

وscratchpad/win/run.sh هو النصف الآخر من هذا وليس بوابة: يُطلق ملف ويندوز التنفيذي المحزوم تحت Wine مع --remote-debugging-port=9333، وهو الطريق الوحيد لرؤية مُصيّر ويندوز الحقيقي — أشرطة تمريره وأحداث مؤشّره واستعلامات وسائطه — من غير جهاز ويندوز. شغّل التأكيدات نفسها عبر CDP هناك قبل أي إصدار يمسّ القشرة.

npm run check-keymap: دفتر الارتباطات

تصادم الاختصارات أهدأ عيب يمكن أن يصيب هذا المنتج. معالجٌ يجيب عن المفتاح، والآخر لا يرى الحدث أبدًا، ولا يعلم أيٌّ منهما بوجود الآخر؛ فيظهر العيب بعد أسابيع على هيئة «Ctrl+B لا يفعل شيئًا»، على منصة واحدة، من قارئ واحد، ولا شيء يُبحث عنه، لأن لا خطأ في أيٍّ من الارتباطين. الخطأ أن هناك اثنين.

فالارتباط يوجد في مكان واحد: جدول GROUPS في client/components/ShortcutsHelp.tsx، وهو الجدول نفسه الذي يطبعه Ctrl/Cmd /. وتحلّل هذه البوابة الجدول من نص المصدر (ولا تستورده أبدًا، فالصفوف تحمل إغلاقات React والمخزن، والبوابة التي تحتاج إلى متصفح بوابة لا يشغّلها أحد)، وتحوّل keys كل صف إلى صيغة موحّدة، وتفشل حين يؤدّي صفّان إلى المفتاح نفسه والمعدّلات نفسها والنطاق نفسه. والنطاق هو القشرة (app / blog) وبيئة التشغيل (متصفح / سطح مكتب)، وعمدًا ليس admin: فجلسة المشرف ترى صفوف الزائر مضافًا إليها صفوفها، فلا يفصل admin بين ارتباطين أبدًا؛ وإنما يسمّي القارئ الذي يبلغه التصادم أولًا.

وثمّة تداخل واحد حقيقي، مسوَّغ ومُعلَن: Ctrl/Cmd Shift Z هو الصفاء وهو أيضًا اختصار الإعادة الوحيد في CodeMirror على macOS، ويفصل client/App.tsx بينهما بموضع المؤشر. وإعلان تداخل كهذا يكلّف فقرةً في RESOLVED (client/keymap.ts) تقول أين يُفصل، والإعلان الذي يتوقف عن التصادم يُفشل البناء هو الآخر؛ فالاستثناء الميت ادّعاء يصدّقه القارئ التالي.

ونصفها الثاني هو docs/keymap.md، وهو عرض للدفتر لا نسخة ثانية منه: تقارن البوابة الأوتار في الجداول بين <!-- keymap:begin --> و<!-- keymap:end --> مع GROUPS، في الاتجاهين. أما الأسطح التي لا ضغطة مفتاح فيها (نقرة، أو قائمة الشرطة المائلة، أو سحب في المخطط) فتعيش تحت علامة النهاية، حيث تتركها البوابة وشأنها. ويشغّل tests/keymap.test.ts الكود نفسه من غير ملفات تُكتب.

والنصف الثالث هو ما لا يستطيع دفترٌ متّسق أن يثبته: أن المفتاح يفعل شيئًا. فكل صف يحمل keys يجب أن يكفله تعليق // keymap: <التسمية> على الكود الذي يجيب عنه — الفرع في client/globalKeys.ts (مستمع النافذة الذي تركّبه القشرتان)، أو مدخل خريطة مفاتيح CodeMirror، أو مستمع المكوّن نفسه؛ وخريطة المفاتيح الآتية من مكتبة (السجل، البحث، الطيّ) تُعلَّم حيث يثبّتها المحرّر. والصف الذي لا علامة له يُفشل البوابة (NO HANDLER)، وكذلك العلامة التي تسمّي تسمية لم تعد صفًّا. فقد بقي Ctrl/Cmd Alt L (قلب الملاحظة إلى توأمها) في الورقة وفي صف لوحة الأوامر وفي هذا الدليل بلا معالج على الإطلاق حتى 3.26.1، لأن شيئًا لم يربط الصف بكوده.

npm run check-excerpt: بوابة الوسم في النثر

القاعدة الصارمة في DESIGN.md أن المقتطف المعروض خارج المحرر إما أن يجرّد ماركداون وإما أن يعرضه. أما حذف # وترك الكلمة المجرّدة واقفة في الجملة فليس هذا ولا ذاك، وقد وصل إلى القرّاء فعلًا: فتدوينة تنتهي بـ«…it buys the reader a breath. #design #typography» طُبعت على الصفحة الأولى «…it buys the reader a breath. design typography». والأسطح الثلاثة التي تمرّ عبر مجرِّد واحد (stripInlineMd) تُختبر كلها من ملاحظة تجريبية واحدة ينتهي متنها بسطر وسوم: مقتطف التدوينة (/api/posts: بطاقات المدونة، وRSS، وog:description)، ومقتطف البحث (/api/search)، وسطر سياق الرابط الراجع (/api/backlinks). وتفحص الاتجاه الآخر أيضًا، أي أن الجملة المجرَّدة تنجو، وأن الوسوم ما زالت تظهر حيث ينبغي للوسوم أن تظهر (post.tags، وفهرس البحث ما زال يطابقها)، فالمجرِّد الذي ينجح بحذف كل شيء يفشل هو الآخر. ولا حاجة إلى متصفح؛ وتحذف ملفاتها التجريبية كيفما انتهى التشغيل.

npm run check-design: حاجز الأخطاء

بوابة الوعد الوحيد في محرّك التصميم الذي لا يمكن مراجعته بقراءة الكود: أن القسم الذي يتعطّل لا يُسقط الصفحة كلها معه. تكسر موقعًا مصمَّمًا بثلاث طرق عمدًا (ملف designs.json فاسد، وقسم يشير إلى ملاحظة غير موجودة، ومعرِّض قسم مرقّع ليرمي خطأ ثم أُعيد بناؤه)، وتقيس في كل حالة ما يأخذه الزائر (المدونة المدمجة، وصفحة فيها نص حقيقي، ولا شيء يفلت من الحدّ) مقابل ما يأخذه المالك (الصفحة المصمَّمة، والقسم الفاشل مسمًّى، وزر الرجوع حاضر). وتذهب وتعود بين الوضعين الجاهز ⇄ المصمَّم وتؤكد أن التصميم يعود مطابقًا بايتًا ببايت. ويُستعاد كل ما مسّته في طريق الخروج، حتى عند الفشل: PORT=6801 ASTROLABE_PASSWORD=… npm run check-design.

npm run check-board: لوحة أقسام المصمم

ثلاث طرق لنقل صف (زرّا ↑/↓، وسحب بالمؤشر، ورفع بلوحة المفاتيح بـSpace ثم الأسهم ثم Space)، ومعاينة الإسقاط قبل التثبيت، وانتماء Esc إلى الطبقة الأعمق، وعدّاد شريط الحفظ، وذهاب وإياب بـCtrl/Cmd S عبر المخزن.

npm run check-preview: معاينة المصمم الحية

أنها iframe حقيقي تحت frame-src 'none'، وأن الأنماط والسمة تصلان إليها (بما في ذلك تبديل السمة حيًّا)، وأنها تتخطّط على عروض أجهزة 390 / اللوحي / 1440.

npm run check-print: الصفحة المطبوعة

PORT=6801 npm run check-print. السطح الوحيد الذي لا ينظر إليه أحد وهو يعمل: فقواعد @media print خفيّة عن كل أدوات اللقطات أعلاه، لأن المتصفح لا يطبّقها إلا حين يفتح إنسان نافذة الطباعة. فتقود هذه البوابة التطبيق تحت emulateMedia("print") وتؤكد أن مضيف الطباعة هو الشيء الوحيد على الورق (وأنه display: none على الشاشة فلا يومض أبدًا)، وأن لوحة ألوان الورق تغلب السمة الداكنة، وأن التنبيه المطويّ يطبع متنه، وأن العناوين تبقى h1–h6 حقيقية بمعرّفاتها وأن الروابط الداخلية تحتفظ بقيم href التي تبدأ بـ# (وهما الشيئان اللذان يبني منهما Chrome مخطط إشارات PDF المرجعية وحواشي روابطه)، وأن الملاحظة العربية تُطبع صفحةً من اليمين إلى اليسار من نسخة إنجليزية. وتكتب ملاحظتين تجريبيتين عبر الواجهة البرمجية وتحذفهما في طريق الخروج. انظر الطباعة وPDF.

npm run check-deck: كل شريحة في «ما الجديد»، مقيسة

CHROMIUM=/usr/bin/chromium npm run check-deck -- http://127.0.0.1:6801 <كلمة مرور المشرف>. عرض «ما الجديد» رسوم وعروض حية، والرسم قد يبدو صحيحًا باللغة التي رُسم بها ثم يخرج عن إطاره باللغة الأخرى. تفتح هذه البوابة عرض كل إصدار بالإنجليزية ثم بالعربية، وتجمّد الحركة عند اللحظة التي وصل فيها كل جزء، وتفشل إن خرج أي نص من الإطار، أو جلس خارج الزر الذي يخصه، أو تراكب مع نص آخر، أو كان نصًا عربيًا مضبوطًا من اليمين إلى اليسار داخل رسم، أو جملة عربية داخل رسم (النثر مكانه عرض DOM). شغّلها قبل كل إصدار يضيف شريحة.

npm run check-presets: كتالوج الإعدادات المسبقة

معرّفات فريدة، واسم ونبذة بلغتين بعربية حقيقية، وعائلة معروفة، وإعداد مسبق واحد على الأقل لكل عائلة، ولا إعداد مسبق يسمّي ملاحظة في خزانة أحد. وتشغّل assertCatalog المشتركة بدل أن تعيد تنفيذها.

npm run check-docs: الدليل

هذه الصفحة وكل صفحة سواها، باللغتين. تمشي البوابة على كل رابط في README.md وdocs/*.md وdocs/ar/*.md بمحلّل ماركداون (فتُترك صيغة الرابط المقتبسة بين علامات الكود وشأنها) وتحلّ كلًّا منها على الشجرة: الرابط النسبي يجب أن يصل إلى ملف، والمرساة #anchor يجب أن تسمّي عنوانًا في الصفحة التي تشير إليها، والصورة يجب أن تكون على القرص. وتُحلّ المراسي بقاعدة الرمز الواحدة في scripts/build-docs.mjs، وهي قاعدة GitHub نفسها، إذ تُحذف علامات الترقيم ولا تُستبدل بشرطات وتبقى الحروف العربية، فالرابط الذي يجتاز هنا يصل على الموقع وعلى GitHub معًا. وكل مسار إعدادات في النثر (الإعدادات ← تبويب ← صف) يُقرأ على مصدر اللوحة نفسه (جدول التبويبات وعناوين المجموعات وفهرس الصفوف، مع حلّ التسميات عبر client/i18n.ts بلغة الصفحة)، وكل صفحة في جدول الموقع يجب أن توجد بالعربية وببنية العناوين نفسها التي لتوأمها الإنجليزي. ويشغّل tests/docs.test.ts الدالة نفسها تحت npm test.

npm run check-settings: فهرس الإعدادات

يُولَّد client/components/settings/settingsIndex.ts، الذي يقرأه بحث اللوحة، من مصدر اللوحة بالأمر node scripts/gen-settings-index.mjs؛ وتفشل هذه البوابة حين يختلف الملف المودَع عن المصدر، وهذا هو السبيل الوحيد لانحرافهما. فبحث يكفّ في صمت عن إيجاد صف أسوأ من لا بحث.

npm run check-whatsnew: مجموعة الشرائح

كل إصدار فرعي (x.Y.0) يجب أن يُدرج في client/whatsnew/versions.ts وأن تكون له مجموعة شرائح فيها شريحة واحدة على الأقل في releaseNotes.ts، وكل عنوان ومتن في كل شريحة يجب أن يحمل اللغتين. والتصحيح يرث مجموعة إصداره الفرعي. ورفع الإصدار من غير مجموعة يفشل هنا، وهذا هو التذكير.

npm run check-a11y: إمكانية الوصول الساكنة

تحفظ الخط الذي رسمه التدقيق، من المصدر وحده: لا outline: none من غير حلقة تركيز بديلة في القاعدة نفسها، واسم يمكن الوصول إليه لكل عنصر تحكم لا يحمل إلا أيقونة، وسائر القائمة في أعلى السكربت. وهي مثل check-i18n قائمة من أجل صنف التراجعات الذي لا يُرى في المراجعة ولا في لقطة الشاشة.

npm run check-cascade: لا قاعدة للهاتف تبطلها قاعدة بعدها

بلا متصفح ولا خادم. الهاتف وهيكل اللمس كتلُ @media مكتوبة فوق قواعد سطح المكتب، ولا تغلب الكتلة إلا إذا جاءت بعد ما تعدّله. وقد شُحنت قاعدة للهاتف ميتةً خمس مرات لأن قاعدة للمحدِّد نفسه والخاصية نفسها جاءت بعدها في التسلسل بلا أي شرط: عدّاد الموضع في «ما الجديد» (يظهر للهاتف ثم يُخفى للجميع بعد ثمانية عشر سطرًا)، والتفاف محرّر جذور المكتبة، وثلاثة تصريحات في الإعدادات (يقولها app.css ثم يعيد settings.css قيم سطح المكتب لأنه يُحمَّل بعده)، وزر الترتيب في رفّ الوسوم، وأزرار الربط في الإشارات غير المربوطة. كل ملف يُقرأ صحيحًا وحده، والخاسر لا يُطبَّق أبدًا، فلا يراه فرق التعديلات ولا مراجعة اللقطات.

تقرأ البوابة كل client/styles/*.css بترتيب المتصفح: الأوراق التي يربطها client/index.html بترتيب ربطها، ثم كل ورقة تستوردها وحدة برمجية (وهذه تأتي دائمًا بعد المربوطة، بترتيب لا يستطيع السكربت معرفته، فلا تُقارَن ورقتان مستوردتان إحداهما بالأخرى). وتفشل عند تصريح داخل كتلة هاتف أو لمس (@media تسأل (pointer: coarse) أو (hover: none) أو max-width بألف بكسل فما دون) تعود قاعدةٌ لاحقة بلا @media حولها فتضبط خاصيته للمحدِّد نفسه. ويُحسب !important كما يحسبه التسلسل؛ والخاصية المختصرة تبطل خصائصها المفصّلة، والخاصية المنطقية تقابل توأميها الفيزيائيين (padding تبطل padding-inline، وmin-height تبطل min-block-size). أما القاعدة اللاحقة تحت تفضيل القارئ — prefers-reduced-motion وforced-colors — فأضيق من كتلة الهاتف وتغلبها عمدًا، فلا تُعدّ فشلًا. والمحدِّدات المختلفة التي تبلغ العنصر نفسه تقيسها check-phone. ويطبع --list كل ما يجده من غير أن يفشل.

ورفيقها tests/breakpoints.test.ts، الذي يُلزم كل ورقة أنماط تحت client/ وmobile/src بعرض هاتف واحد: كل max-width بين 601 و799 بكسل هو 700 الخاص بالهيكل (PHONE_SHELL_QUERY في client/shellQuery.ts)، إلا أن تقول الأسطر الثلاثة فوقه لماذا لا بعبارة "not the shell's 700" — وهذا ما يفعله 640 في المدوّنة العامة، لشبكاتها الخاصة — ولا يكتب ملف مصدري استعلام الهاتف بحروفه بدل أن يستورد الثابت.

npm run check-phone: هيكل الهاتف تحت الاختبار

بوابة متصفح: CHROMIUM=/usr/bin/chromium ASTROLABE_PASSWORD=<كلمة المرور> npm run check-phone -- <العنوان> [مجلد اللقطات]، على خادم تجريبي. تقود هيكل الهاتف (client/phone/) كما يقوده إبهام القارئ، بالإنجليزية والعربية، على ستة أشكال، وتصوّر كل شاشة وكل ورقة تمرّ بها.

ما تفعله، تأكيداتٍ. نقرة على مجلد تدفع المجلد؛ ونقرة على ملاحظة تغيّر العنوان في الشريط وعنوان الصفحة (كانت البوابة القديمة خضراء يوم كانت نقرة الشجرة على الهاتف لا تفتح شيئًا)؛ وشاشة الملاحظة بلا شريط أبواب؛ وورقة الملاحظة تأخذ مدخلًا في السجل، ورجوع المتصفح يغلقها قبل أن يُخرج الملاحظة؛ والنشر يسأل، والنشر الملغى لا ينشر شيئًا؛ ورمز الوضع يبدّل الوضع الذي يسمّيه؛ والرجوع يعيد الملاحظة إلى مجلدها؛ والضغط المطوّل يرفع ورقة إجراءات الصف والرجوع يغلقها؛ والبحث مركَّز عند الوصول؛ ويوم التقويم يُفتح ورقةً؛ والرابط العميق يفتح ملاحظته والرجوع منه يعود إلى اليوم؛ ولا أخطاء في الصفحة. ومنذ 3.27.0 تُسأل كل شاشة من شاشات الجولة الثانية ما يسأله الإبهام: المدارات تسرد مجموعاتها، والمجموعة تُفتح، وادرس يبدأ الجلسة على الشاشة كلها (وعلى اللوح تأخذ العمودين) والرجوع يعيد إلى المجموعة؛ والسِّجِلّ يسرد سجلّاته، والسجلّ يُفتح والتأشير يستجيب فورًا ويحفظه الخادم؛ والوسائط تسرد رفوفها والمتابعة تُفتح بطاقتَها؛ وللكتاب شريط واحد خاص به (لا شريط سطح المكتب ولا الشريط العلوي للهاتف) ومنزلقه يحرّك الصفحة؛ و⋯ القارئ ورقة إجراءات والرجوع يغلقها ويُبقي الكتاب؛ ومنتقي السمة (طبقة على <body>) يأخذ مدخلًا والرجوع يغلقه؛ والإعدادات قائمة، والقسم شاشة، والتعديل يرفع شريط الحفظ، والرجوع مع تعديل يسأل والإلغاء يُبقي التعديل، والحفظ يكتبه؛ ومنتقي الوسوم يُفتح فوق ورقة الملاحظة ويكتب الوسم في الملاحظة؛ ويعود المجلد ممرَّرًا إلى حيث تُرك. ومنذ 3.34، وهو بلاغ قارئ على Galaxy Z Fold، يُسأل أولًا: على عمق مجلدين، إعادة التحميل تُبقي المجلد وسهمه ينزل على المجلد الأب، وكذلك بعد رحلة إلى «اليوم» والعودة، وإيماءة الرجوع في النظام تصعد من هناك لا إلى البداية؛ وشريط المجلد مساره فُتاتًا ينتهي باسمه ويصعد (عبر «…» حيث ينطوي)؛ وباب الملاحظات يبدأ بالشجرة على عمودين وبالمجلدات على عمود؛ والشجرة تفتح المجلد في مكانه، وتتذكّره بعد إعادة التحميل، وتطوي ملفات المجلد في صف واحد يُفتح في مكانه، وتقول عن المجلد الفارغ إنه فارغ، والضغط المطوّل على صفها قائمته؛ وعلى عمودين تُبقي القائمة تمريرها وصفّها المضاء والملاحظة تتبدّل، والمقبض يوسّع القائمة ويُحفظ العرض، وورقة الملاحظة تنزلق فوق عمود الملاحظة لا فوق القائمة. وCHECK_PHONE_SHAPES=phone,fold-open يشغّل بعض الأشكال أثناء العمل؛ والبوابة هي الستة كلها.

ما تقيسه، على كل شاشة وورقة. لا شيء يفيض جانبًا (ويُستثنى شريط يتمرّر عمدًا)؛ وكل هدف في الهيكل ≥ 44 بكسل (في الارتفاع دائمًا، وفي العرض حين لا يحمل العنصر نصًّا؛ ولا يُعدّ النثر ولا مربّع اختيار داخل <label> بارتفاع 44 ولا خلايا الصورة البيانية أهدافًا)؛ وكل حقل نصّي ≥ 16 بكسل، وتحته يكبّر سفاري في iOS الصفحة داخل الحقل؛ ولا شيء يغطّي هدفًا (elementFromPoint في مركزه يجيب بالهدف). وتحت ورقة مفتوحة تُسأل الورقة وحدها، لأن الصفحة تحتها معطّلة عمدًا. ولوحا «المخطط» و«الروابط الخلفية» في ورقة الملاحظة يُقاسان وفيهما صفوف: فإذا كان للملاحظة عناوين وروابط خلفية (يُسأل الخادم عنها)، تتحقق البوابة أولًا من أن اللوح يسردها، حتى لا يمرّ فحص الـ44 بكسل على لوح فارغ.

الأشكال الستة. phone جهاز Pixel 7 بمقاس 412×915 بإصبع. وfold-cover وfold-open شاشتا Galaxy Z Fold كما يبلّغ عنهما كروم بكثافته 2.625 — الشاشة الخارجية 344×882 والداخلية مفتوحًا 690×829، كلتاهما بعمود واحد (العمودان من 1000 بكسل)، والداخلية بألسنة الملاحظة. وstylus مقاس 720×820 بكثافة 1.5 مع قلم — availablePointerTypes=6, primaryPointerType=2, availableHoverTypes=3, primaryHoverType=1 — وهو إعداد Blink في متصفح مستقل، لأن hasTouch يجعل كروميوم يبلّغ عن جهاز خشن فقط مهما قالت رايات المؤشر؛ وهو الوضع الذي قُدّم له هيكل سطح المكتب يومًا، ويرسم عمودًا واحدًا لأن عرضه عرض الـFold مفتوحًا. وtablet وtablet-land لوح لمس بمقاس 820×1180 (عمود واحد بألسنة) و1180×820، يرسم فيه الهيكل عموده وقائمته والملاحظة، وتنزلق ورقة الملاحظة من الجانب. وبعد المصفوفة، الملاحظة تأخذ الصفحة: عند 344 و690 و829×690 و768 و820 و1024 و1180، باللغتين، الملاحظة المفتوحة 90% من العرض على الأقل تحت 1000 بكسل؛ وفوقها القائمة 360 بكسل على الأكثر وإخفاء القائمة يجعل الملاحظة 90% على الأقل، ويبقى بعد إعادة التحميل؛ ونافذة 690 تتسع إلى 1180 ثم تعود تُبقي ملاحظتها مفتوحة؛ ومن 600 بكسل الملاحظة الثانية المفتوحة من القائمة لسان ثانٍ، والأولى على بُعد نقرة، و**×** يُظهر جارتها، وإعادة التحميل تُبقي الاثنين.

npm run check-shell-seam: هيكلان بلا إطار مشترك

بوابة ساكنة. يشترك هيكل الهاتف وهيكل سطح المكتب في المخزن وطبقة الواجهة البرمجية والقاموس وكل أسطح المحتوى، ولا يشتركان في شيء من إطار أحدهما: فلا يستورد client/phone/ ملف app.css ولا شريط ألسنة سطح المكتب ولا شبكة لوحاته ولا مقابضه ولا شريط حالته ولا شريطه الجانبي؛ ولا يستورد client/components/ شيئًا من client/phone/؛ ولا يسأل client/phone/phone.css عن العرض، لأن التركيب نفسه هو الشرط. ويشغّل tests/phoneShell.test.ts القواعد نفسها.

npm run check-bundle: ما ينزّله كل جمهور

بعد npm run build. يشحن العميل جزءًا للمدخل وجزءًا لكل سطح، ولا معنى لهذا التقسيم إلا إذا صمد: استيراد واحد غير مبالٍ في أعلى ملف يحمّله المدخل أصلًا يعيد هيكل التطبيق كله إلى أول طلب لقارئ مجهول. تقيس البوابة تنزيل كل جمهور على ميزانية؛ ولا تتحرك الميزانية إلا بمقدار التجاوز الفعلي، مع كتابة السبب بجانبها. والقاموس جزءان، واحد لكل لغة، وليس أيٌّ منهما في المدخل: يُقاس كل جمهور ومعه لغة واحدة، الكبرى منهما، لأن الصفحة تنزّل اللغة التي تتكلمها.

npm run check-perf: بوابة الأداء

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

فالبوابة تجلب خزانتها. يولّد scripts/perf-fixture.mjs خزانةً من بذرة ثابتة: 2000 ملاحظة في 40 مجلدًا بمقدّمات وروابط ويكي ووسوم، وملاحظةٌ من 3000 سطر، وملاحظةٌ فيها خمسون تضمينًا، وسنةٌ من الملاحظات اليومية، واثنا عشر سجلًّا تحت كلٍّ منها سجلُّ معظم سنة. ثم يشغّل check-perf خادمه الخاص فوقها، على منفذه الخاص، في الوضع المحلي المفتوح. ولا يأخذ مسار خزانة قط، ولا يقرأ المولِّد قرص أحد، فلا يمكن توجيه أيٍّ منهما إلى خزانتك. واضبط ASTROLABE_SEED_VAULT=<خزانة> إن شئت أن يُضمّ إليها كتابٌ حقيقي وملاحظات الطلاسم والمدارات الحقيقية؛ واتركه غير مضبوط فتقيس البوابة الخزانة المولَّدة، وهي الخزانة نفسها على كل جهاز.

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

الميزانيةما تلتقطه
أول رسم لتطبيق المالكاستيراد ساكن يجرّ سطحًا كسولًا إلى أول طلب للهيكل
ضغطة المفتاح ← الرسم في ملاحظة الـ3000 سطر، الوسيط والمئين 95مرور لكل ضغطة صار في صمت بتعقيد الوثيقة كلها
زمن المهام الطويلة خلال دفعة من 40 ضغطةالإخفاق نفسه، مقيسًا عملًا لا موقعًا من حدّ الإطار: أحدّها، وأكثرها تحرّكًا بالتطهير
رسم وضع القراءة لتلك الملاحظةالشيء نفسه، للعملية الوحيدة التي كلفتها الوثيقة كلها دفعةً واحدة
صفحة السِّجِلّ، من الباب إلى رسم البطاقات الاثنتي عشرة (الميزانية 1650 مللي ثانية؛ 1241 حين وُضعت)بطاقة عادت سلسلتها أو خريطتها الحرارية أو أسبوعها إلى المشي في السجل كله مع كل رسم
صفحة التقويم، من الباب إلى رسم الشهر بأسطره ونقاط ملاحظاته اليومية (250؛ 157 حين وُضعت)خانة يوم تقرأ الخزانة بدل جدول اليوم الذي حسبته الصفحة مرة واحدة
شجرة الهاتف، من النقرة إلى فتح مجلد فيه خمسون ملاحظة (100؛ 70 حين وُضعت)، وأطول مهمة على الخيط الرئيس في ست عشرة نقرة (لا شيء)شجرة تمشي في الخزانة كلها بدل الصفوف الظاهرة، أو تقيس نافذتها بعد أن تدخل الصفوف المستند

زمن الكتابة هو قياس المتصفح نفسه لتوقيت الأحداث: من ضغطة المفتاح العتادية إلى الرسم الذي يُظهر الحرف، لا عدّاد إطارات؛ ويوضع المؤشر أولًا عند السطر 1500 تقريبًا، لأن الكتابة في السطر الأول من ملاحظة طويلة تقيس ملاحظة قصيرة. وتتحرك الميزانيات كما تتحرك ميزانيات check-bundle: بمقدار التجاوز الفعلي مع كتابة السبب بجانبها، أو نزولًا حين تستحق جولةٌ ذلك. وPERF_ROUNDS=1 هي الصيغة السريعة، وPERF_KEEP=1 يُبقي الخزانة المولّدة لتفحصها.

npm run check-media: شريحة المجلد

في متصفح، على نسخة تعمل (node scripts/check-media.mjs http://localhost:6801، ومعه ASTROLABE_PASSWORD إن كان للنسخة كلمة سر). تكتب متتبعين عبر الواجهة البرمجية، أحدهما على مجلد عادي فيه ملاحظتان والآخر على مجلد له ملاحظة باسمه، وتفتح صفحة الوسائط وتضغط شريحة المجلد في كل منهما: يجب أن يكون صف المجلد في الشجرة، منفرجًا، مُمرَّرًا إلى الشاشة، ومرسومًا — من غير أن تحمل الشجرة تركيز لوحة المفاتيح، وهو ما لا تعطيه النقرة في صفحة الوسائط أبدًا — والمجلد الذي له ملاحظة باسمه يجب أن يفتح ملاحظته أيضًا. وتُحذف الأمثلة حذفًا نهائيًا كيفما انتهى التشغيل.

npm run check-books: القارئ

بعد npm run build. عشر خصائص لقارئ الكتب — بسطحيه: قارئ PDF وقارئ EPUB — لا تُرى في المراجعة ويغلو اكتشافها في الإنتاج، أولها أن عامل pdf.js أصلٌ حقيقي من الأصل نفسه لا عنوان blob:، وهو ما يعمل تحت خادم التطوير الذي لا سياسة أمن محتوى له ويموت تحت السياسة الحقيقية.

npm run check-icons: علامات المجلدات

يُرسم shared/folderIconNames.ts وshared/folderIconPaths.ts من الكتالوج بالأمر npm run gen-icons؛ وهذه صيغة --check، تفشل حين يكون الرسم قديمًا.

npm run check-signatures: كل بيت موقّع

بوابة متصفح ترسم كل بيت موقّع عبر المصيّر العام الحقيقي على واجهة برمجية تجريبية معزولة، من غير مساس بخزانة حية أو إعدادات أو تصميم محفوظ أو حساب. ويضيّقها SIGNATURES=a,b، ويرسم THEME=<id> كل بيت في سمة واحدة، ويكتب SHOTS=1 لقطات الشاشة.

npm run check-hovercache: ذاكرة بطاقات التحويم

بوابة متصفح تثبت أن حدّ LRU لمعاينات التحويم يصمد في جلسة حقيقية: الذاكرة مفهرسة بمسار الملاحظة، فمن غير CACHE_MAX كانت أمسية من تصفح الروابط تحتفظ بكل ملاحظة تصفحتها. والحدّ الذي لا يؤكده إلا ثابت حدٌّ يكفّ في صمت عن أن يكون صحيحًا.

npm run check-designer-nav: تنقل المصمم ومحاذاته

بوابة متصفح وُلدت من علّة شُحنت متجاوزةً كل فحص آخر، بلغة المالك نفسه: معاينة مُصغَّرة بـtransform-origin مادي داخل تخطيط منطقي تجلس بعيدًا عن صندوقها في [dir="rtl"]. تقيس كل سطح في المصمم في الاتجاهين، وتمشي على تنقل المصمم.

scripts/check-pdfsearch.mjs: البحث داخل كتاب

سكربت مجرد لا مدخل له في package.json: CHROMIUM=/usr/bin/chromium node scripts/check-pdfsearch.mjs <url> <password>. على خادم تحمل خزانته ملف PDF فيه كلمة لا تظهر في أي ملاحظة، يثبت الحلقة كلها: تجيب الواجهة البرمجية بصف kind: "book" يسمّي الصفحة، ويرسمه الشريط الجانبي، والنقر عليه يفتح القارئ على تلك الصفحة والكلمة موجودة.

scripts/check-desktop-boot.sh وcheck-desktop-relaunch.sh: بوابتا سطح المكتب

تأخذ كلتاهما ملف AppImage وتقلعه تحت Xvfb (شاشة افتراضية) بمجلد إعداد معزول، من مجلد مؤقت فارغ (لا من المستودع أبدًا: فالتطبيق الذي يبدأ بجانب ملف .env يربط نفسه بذلك النشر)، فوق خزانة مؤقتة فارغة يسمّيها ASTROLABE_VAULT. وتفشل بوابة الإقلاع عند استثناء غير ملتقَط أو خطأ نحوي في الثواني الخمس والعشرين الأولى؛ فقد كانت البناءات 3.1.0–3.3.4 تنهار عند التحميل ولم يقل شيء ذلك. وتضبط بوابة إعادة التشغيل المتغيّر ASTROLABE_SELFTEST=relaunch، الذي يجعل التطبيق يعيد تشغيل نفسه بعد أربع ثوانٍ من الإقلاع بالطريقة نفسها التي يفعلها التحديث المطبَّق بالضبط، ولا تنجح إلا حين تكون العملية الأولى قد ذهبت وتكون عملية ثانية بدأت من الملف نفسه تعمل؛ فقد بدا app.relaunch() كأنه يعمل ولم يكن يعمل، لأن معيد التشغيل في Electron يعمل من الصورة المركّبة بعد فكّ تركيبها. وكل إصدار AppImage يشغّل البوابتين قبل الرفع.

اختبارات التطابق: قاعدة واحدة في كل جانب

بعض الوعود ليست لسكربت بل لمجموعة الاختبارات، لأنها تقوم بين قطعتين من الكود يجب أن تتفقا. ويسرد contracts/gates.md كل البوابات؛ وهذه هي الاختبارات التي تُلزم بقاعدة واحدة المواضعَ التي كان كلٌّ منها يحفظ نسخته:

الاختبارما يحفظه
tests/pocketParity.test.tsفهرس الجيب على الهاتف وفهرس الخادم يعطيان الأجوبة نفسها على خزانة اختبار واحدة: حلّ الروابط، والأسماء البديلة، والوسوم، والرايات، والروابط الخلفية، والبحث، وترتيب الشجرة.
tests/headings.test.tsقاعدة عناوين واحدة (shared/headings.ts): ما يقترحه المحرر بعد [[Note#، والمخطط، وجدول المراسي، ومعرّفات وضع القراءة قائمة واحدة، وتعليق YAML الذي يبدأ بـ# ليس عنوانًا أبدًا.
tests/byteRange.test.tsمحلّل Range: واحد (shared/byteRange.ts) خلف مساري /api/file كليهما، والطلبات الخمسة نفسها تُجاب بالطريقة نفسها في كلٍّ منهما.
tests/fileTypes.test.tsجدول واحد لأنواع الخدمة، وجدول واحد للأصناف، وفحص واحد للصور (shared/attachments.ts)، وترتيب واحد للشجرة (shared/tree.ts)، ويوم محلي واحد (shared/dates.ts) — ولا نسخة شاردة من الأخيرين في الكود.
tests/breakpoints.test.tsعرض هاتف واحد في كل ورقة أنماط (أعلاه).
tests/i18nScan.test.tsفحص النصوص يرى ما لم يكن الفحص السطري يراه (أعلاه).
tests/rtlGlyphs.test.tsالرمز المعكوس ثنائي الاتجاه (‹ › « ») يُعكس مرة واحدة: فعكسُ المتصفح له يتوقف على الخط، ولذا فالرمز الذي يُقلب باليد تحت الاتجاه من اليمين إلى اليسار يُثبَّت من اليسار إلى اليمين أولًا.
tests/sourceText.test.tsلا يحمل ملف مصدري محرف تحكّم حرفيًّا، فلا يسمّيه grep ملفًا ثنائيًّا أبدًا.

الأداء

ما قيس، وعلى ماذا، وكم كلّف قبلُ وبعدُ، وما بقي بطيئًا ولماذا.

الخزانة المرجعية

يُقاس الأداء هنا على خزانة لا يملكها أحد، لأن الخزانتين الموجودتين كلتيهما لا تصلحان له: البذرية عشرُ ملاحظات وكل شيء فيها فوري، وخزانة المالك خاصة ولا تُقدَّم من خادم أبدًا. فيولّد scripts/perf-fixture.mjs الخزانة الثالثة من بذرة ثابتة، حتى يكون الرقم المأخوذ اليوم قابلًا للمقارنة برقم يؤخذ بعد شهر:

  • 2000 ملاحظة في 40 مجلدًا، لكل واحدة ستّ خصائص في المقدّمة وخمسة [[روابط ويكي]] وثلاثة #وسوم: أي 110 وسمًا متمايزًا في شجرة، و2000 عقدة للمخطط؛
  • ملاحظة من 3000 سطر (291 ألف محرف، 54 ألف كلمة): أسوأ حالة صادقة للمحرر؛
  • ملاحظة فيها خمسون ![[تضمينًا]]؛
  • سنة من الملاحظات اليومية، حتى يكون لصفحة التقويم شهرٌ في كل خلية منه شيء؛
  • ثم، حين يسمّي ASTROLABE_SEED_VAULT خزانةً تُؤخذ منها وحدها، ملف PDF من 665 صفحة حقيقي وملاحظات الطلاسم والمدارات الحقيقية: قارئٌ ورفّان لهم ما يرسمونه. وهو اختيارٌ صريح يُعلنه المولِّد في مخرجاته، لأن خزانةً تمدّ يدها إلى خزانة لم يسمّها أحد تجمع سوأتين: خادمٌ تجريبي بلا كلمة سرّ فوق ملاحظات أحدهم الخاصة، وميزانيةٌ لا يبلغها إلا جهازٌ واحد. وقد كانت الثانية بالقياس: الخزانة المولَّدة تحمل 110 وسمًا في كل مكان، و121 على الحاسوب الذي قيس عليه التطهير.

وقد أُجري المسح أدناه وهذا المتغيّر مضبوط، فكانت خزانته 2376 ملاحظة بدل 2367 التي يكتبها المولِّد وحده. والملاحظات التسع مذكورة لا مخفيّة، ولا شيء في الجدول يقوم عليها.

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

وتحفّظٌ واحد على كل جزء من الألف من الثانية في هذه الصفحة: الجهاز الذي قيس عليه يحمل عادةً متوسط حمل بين 13 و17، والأرقام المخنوقة تتحرك معه. وكل ما أدناه أُخذ تباعًا على الجهاز نفسه وفي الحالة نفسها، وهو ما يجعل «قبل» و«بعد» قابلَين للمقارنة؛ أما القيم المطلقة فهي قيم حاسوب مشغول لا منصّة قياس. وحيث يهمّ الحمل ميزانيةً، يقول scripts/check-perf.mjs ذلك بجانب تلك الميزانية.

الأرقام

قبلبعد
الكتابة، ملاحظة 3000 سطر: ضغطة ← رسم، الوسيط32 مللي ثانية24 مللي ثانية
… المئين 9548 مللي ثانية32 مللي ثانية
… زمن المهام الطويلة في الخيط الرئيس خلال 40 ضغطة571 مللي ثانية184 مللي ثانية
الكتابة، ملاحظة الخمسين تضمينًا: معالج الإدخال، الوسيط18.0 مللي ثانية6.5 مللي ثانية
وضع القراءة، ملاحظة 3000 سطر، الرسم1673 مللي ثانية1081 مللي ثانية
تطبيق المالك: أول رسم1008 مللي ثانية936 مللي ثانية
… جافاسكربت في أول طلب (check-bundle)1551.7 كيلوبايت1048.7 كيلوبايت
… الزمن حتى التفاعل (نهاية آخر مهمة طويلة)1638 مللي ثانية1516 مللي ثانية
الموقع العام: أول رسم608 مللي ثانية548 مللي ثانية
GET /api/props (رفّ الخصائص)28.3 مللي ثانية0.9 مللي ثانية
GET /api/tags1.1 مللي ثانية0.9 مللي ثانية
صفحة الطلاسم، الفتح2861 مللي ثانية2628 مللي ثانية
صفحة التقويم، الفتح2265 مللي ثانية2182 مللي ثانية

ولم يتغيّر، وقيس ليُعرف أنه لم يتغيّر: فتح الشجرة لأربعين مجلدًا دفعةً واحدة (974 ← 963 مللي ثانية)، وتقليب لسان رفّ الوسوم (39 ← 43 مللي ثانية)، ورسم كل ضغطة في البحث (16 ← 16 مللي ثانية)، وحفظ ملاحظة واحدة وإعادة فهرستها على الخادم (7.0 ← 7.7 مللي ثانية)، ووصول دفعة من 300 ملف إلى الفهرس (353 ← 347 مللي ثانية)، وبدء الفهرس البارد فوق 2376 ملاحظة (1276 ← 1264 مللي ثانية). وبقيت الذاكرة مستوية في الحالتين بعد أربعمئة تنقّل خلال عشر دقائق (−6.2 ميغابايت و+1.2 ميغابايت بعد جمع قسري): جولةٌ على المنتج كله لا تحتفظ بشيء.

وما حرّكه التطهير فعلًا، بالترتيب الذي استحقّ التحريك به:

  1. كانت الخزانة كلها تُمشى مرةً لكل رابط ويكي. كان resolveLink ينادي collectNotes، وهي تسطّح الشجرة وترتّبها بـlocaleCompare؛ فرسمُ ملاحظة الـ3000 سطر كان يمشي على 2376 ملاحظة ويعيد ترتيب 2000 منها 176 مرة، مرةً لكل رابط. وهي الآن محفوظة على كائن الشجرة نفسه (المخزن يستبدله ولا يعدّله، فالهوية ختمٌ دقيق)، ومعها جدول أسماء وجدول مسارات يُبنيان مرةً واحدة، وصارت الحلقتان في resolveLink بحثين في خريطة. ذهب بذلك 6.8% من رسم القراءة و2.8% من كل ضغطة مفتاح.
  2. كانت تعليقات كل ملاحظة تُوضع بعد 120 مللي ثانية من كل ضغطة، حتى في الملاحظات التي لا تعليق فيها. كان الراسم يختزل الوثيقة كلها إلى نثر مع خريطة إزاحة لكل محرف، ويطويها، ثم لا يضع شيئًا: فالقائمة الفارغة قيمة صادقة، والحارس فوقها لم يكن يعمل قط. كانت أكبر كلفة منفردة لضغطة المفتاح، تُنفق على لا شيء.
  3. كان شريط الحالة يعيد عدّ الملاحظة عند كل حفظ تلقائي. الشريط يرسم العدّ الحيّ الذي ينشره المخزن المؤقت؛ أما النسخة المجلوبة خلفه فكانت تُجلب وتُعدّ كل 600 مللي ثانية من الكتابة لملء حقل لا يرسمه الشريط أصلًا. وصار يعدّ فقط حين لا يوجد عدّ حيّ يُرسم، وعدّ المخزن المؤقت نفسه محفوظ على الوثيقة.
  4. كانت لوحة المخطط التفصيلي في أول طلب للمالك. وهي تصل إلى جراحة الأقسام ← امتداد التقسيم في المحرر ← محرك زخارف المعاينة الحية ← مصيّر القراءة ← KaTeX. حدٌّ كسولٌ واحد أخرج 503 كيلوبايت وخمسة عشر ملفًا من أول رسم.
  5. كان Intl.DateTimeFormat يُبنى لكل تاريخ. بناء المنسّق يحمّل بيانات ICU، والتنسيق به بحثٌ فقط. وصفحة الطلاسم ترسم تاريخًا لكل بطاقة وتاريخًا لكل خلية في خريطة الحرارة، ومرتين حيث يُعرض التقويمان: 5.9% من فتح الصفحة. والمنسّقات الآن محفوظة بمعاملاتها نفسها، وهو ما كان shared/calendar.ts يفعله للهجري من قبل.
  6. كان رفّا الوسوم والخصائص يُحسبان عند كل طلب. فـprops() تمشي على كل ملاحظة وتقسّم وتقلّم وتطوي حالة الأحرف في كل قيمة مقدّمة؛ ولم يحفظها شيء. وهي الآن تُتحقَّق مقابل shelfRevision() يحرّكه الفهرس، وهي الصفقة نفسها التي يعقدها server/graphCache.ts؛ وذلك هو 28.3 ← 0.9 مللي ثانية أعلاه.

أين يذهب الوقت بعدُ

قيس، وتُرك، لأن الجواب الصادق أن الكلفة حقيقية:

  • تحمل الشجرة 2151 صفًّا في الـDOM حين تُفتح أربعون مجلدًا، وفتحُ الأربعين دفعةً واحدة يكلّف نحو 960 مللي ثانية من مصالحة React وإنشاء الـDOM. وتمريرها إطارٌ نظيف عند 16.7 مللي ثانية، ولا أحد يفتح أربعين مجلدًا بحركة واحدة: مجلدٌ واحد جزءٌ من أربعين من ذلك الرقم. والقائمة غير مُوهَمة: فصفوفها تحمل السحب والإفلات، ومؤشر تبويب متنقّلًا، وaria-posinset على المجموعة المفلترة، ومشيًا بلوحة المفاتيح؛ ووَهْمٌ تحت الأربعة جميعًا يبادل الصحة برقم لا ينتجه قارئ. والسقف لكل مجلد («أظهر N أخرى»، 300 صف) هو الجواب القائم ولا يزال صامدًا.
  • تخطيط المخطط 53% من زمنه، في محاكاة قوى Barnes-Hut مكتوبة باليد فوق مصفوفات مكتوبة، ومعها 17% في drawImage. وهذه محاكاة تؤدي عملها لا علّة؛ تستقرّ 2000 عقدة في نحو ثماني ثوانٍ، واللوحة تظهر في ثانيتين.
  • البحث خاملٌ 91%. فالـ950 مللي ثانية من أول ضغطة إلى أول صف هي 200 مللي ثانية من التهدئة، والكتابة نفسها، وذهابٌ وإياب إلى الخادم في 6 مللي ثانية؛ وكل ضغطة تُرسم في 16 مللي ثانية. والتهدئة هي الميزة.
  • ما بقي من رسم القراءة، 1081 مللي ثانية، تخطيطٌ في معظمه: ستة آلاف عقدة من النثر يقيسها المتصفح. وما فوقه من جافاسكربت صار جزءًا صغيرًا.
  • فتح سطح لأول مرة يكلّف جزءه. فصفحات الطلاسم والمدارات والتقويم نحو 2.2–2.6 ثانية في زيارة باردة، ثلثاها جلبُ الجزء الكسول الذي تعيش فيه تلك الصفحات وتحليله. وهذا هو التقسيم وهو يعمل، لا وهو يفشل.

العمل دون اتصال، مقيسًا كما ينبغي

يُثبَّت عامل الخدمة ويتولّى السيطرة في نحو 250 مللي ثانية، وقراءةٌ قصيرة تترك نحو 88 مدخلًا في ذاكرته. ومع إيقاف الخادم — لا مع محاكاة الانقطاع في المتصفح، فهي تعترض التنقّل فوق العامل ولا تدعه يجيب أبدًا — يرسم التنقّل إلى ملاحظةٍ الهيكلَ المخزَّن في نحو 5 ثوانٍ وعليه شريط «تقرأ نسخة هذا الجهاز». أما الأداة التي تستعمل المحاكاة فتبلّغ عن net::ERR_FAILED ولم تثبت شيئًا؛ انظر العمل دون اتصال.

شريط الإنجاز

التسلسل الذي يمرّ به التغيير قبل أن يُسمّى منجزًا، بهذا الترتيب: npm run typecheck · node scripts/check-i18n.mjs · npm test · npm run build ثم npm run check-bundle · npm run check-perf (على جهاز هادئ) · npm run check-a11y · npm run check-contrast · npm run check-settings (وقبله node scripts/gen-settings-index.mjs حين يتغير صف) · npm run check-keymap حين يتغير مفتاح · npm run check-names · npm run check-shell-seam · npm run check-docs · npm run build-docs · npm run check-desktop حين يتغير electron/ أو desktop/ · npm run check-windows-layout حين يتغير تخطيط القشرة أو اللوحتان أو نقاط انكسارهما · npm run check-cascade كلما تغيّرت ورقة أنماط. ثم بوابات المتصفح التي يمسّها التغيير — وnpm run check-phone كلما تحرّكت ورقة أنماط أو قطعة من الهيكل — مع ضبط CHROMIUM وASTROLABE_PASSWORD، على خادم تجريبي فوق خزانة تجريبية، لا خزانة المالك أبدًا.

أدوات لقطات الشاشة

هذه غير موصولة بـpackage.json؛ شغّلها بيدك للمراجعة البصرية. وكلها تأخذ CHROMIUM، ومعظمها يأخذ THEME=parchment وLANGSET=ar لفحص سمة أو الانعكاس من اليمين إلى اليسار.

الأداةتلتقط
node scripts/shoot.mjs <url> <outdir>المحرر والمخطط ولوحة الأوامر في السمتين
node scripts/shoot-settings.mjs <url> <password> <outdir>كل قسم من الإعدادات عبر الشريط، مع طباعة هندسة تمرير اللوحة وأحجام خطوط العيّنة
node scripts/shoot-sync.mjs <url> <password> <outdir>قسم النسخ الاحتياطي والمزامنة مع لوحة تفاصيل شارة الحالة
node scripts/shoot-themes.mjsكل سمة (ONLY= يضيّقها)
node scripts/shoot-rtl.mjsالانعكاس العربي، ويؤكد ترتيب الرموز في شارات الوسوم
node scripts/shoot-tex.mjsملاحظات .tex، مع ستة تأكيدات
node scripts/shoot-templates.mjsمنتقي القوالب، مع تأكيدات
node scripts/shoot-hover.mjsمعاينات التحويم، مع تأكيدات
node scripts/shoot-controls.mjsمجموعة عناصر التحكم المرسومة يدويًّا (W/H يضبطان نافذة العرض)
node scripts/shoot-fontupload.mjsرافع الخطوط

المساهمة بتغيير

  1. اقرأ DESIGN.md وعقد منطقتك أولًا. يحمل DESIGN.md القواعد التي يُحكم بها على التغيير، وتحمل العقود الوعود التي قطعها الكود من قبل: CONTRACTS.md هو الخريطة، والكلام في contracts/، ملف لكل منطقة (الأساس، والخزانة والخادم، والهيكلان، والسمات، والمحرر، والقراءة، والموقع العام، والميزات، والمزامنة والتطبيقات، واللغتان، والبوابات، وتاريخ الإصدارات). ويُكتب التغيير في القسم الذي يغيّره — عدِّل المنطقة ولا تُلحق شيئًا — ويضيف الإصدار سطرًا واحدًا إلى contracts/releases.md. ومعظم تعليقات المراجعة هنا اقتباسٌ من هذه الوثائق.
  2. شغّل npm run typecheck. فالبناء صارم، وليس للخادم خطوة ترجمة تلتقط الأخطاء لاحقًا.
  3. شغّل البوابات التي يمسّها تغييرك. رموز السمات تعني check-contrast؛ وأي نص يراه المستخدم يعني check-i18n؛ والمخطط أو كود إعادة كتابة الملاحظات يعني check-sections؛ والمحرر يعني check-caret (وcheck-french لكل ما يمسّ الكتابة)؛ والمصمم يعني check-board / check-preview / check-design؛ وأي شيء يقرأ ضغطة مفتاح يعني check-keymap وcheck-layouts، ومعهما tests/shortcuts.test.ts: هل الارتباط فريد، وهل تستطيع لوحة مفاتيح غير لاتينية أن تبلغه؟
  4. اللغتان، والاتجاهان. كل نص يأتي من client/i18n.ts، وكل تخطيط مبنيّ على خصائص CSS المنطقية. والتغيير الذي لا يُقرأ صحيحًا إلا من اليسار إلى اليمين ليس منتهيًا، وLANGSET=ar على أي أداة لقطات أرخص طريقة لرؤية ذلك.
  5. قل «لماذا» في الكود. فتعليقات هذا الكود تشرح القرار لا الآلية. والتعديل الذي يغيّر قاعدة ينبغي أن ينقل الفقرة التي كانت تنصّ عليها.

حرّر هذه الصفحة على GitHub

أسطرلاب برنامج حر. هذه الصفحات مبنية من ملفات Markdown في مجلد docs بالمستودع.