דלגו לתוכן

אינטליגנציית קוד

Control Center נותן לסוכן ארבע דרכים להבין קוד מעבר לקריאה שלו. הן עונות על שאלות שונות לחלוטין, והסיבה שיש ארבע ולא אחת היא שאף מנגנון בודד לא עונה על כולן.

מנגנון עונה על עלות
גרף הקוד מה נמצא כאן, ומה תלוי בו מעבר אינדוקס; שאילתות זולות
שרת שפה האם זה באמת מתקמפל, ומהו הטיפוס הזה תהליך שרת לכל פרויקט
חיפוש מבני היכן מופיעה הצורה הזו ניתוח לכל קובץ
מנפה מה היה נכון ברגע הזה, במסגרת הזו תהליך עצור

גרף הקוד נבנה מ-tree-sitter: סמלים, קצוות קריאה, רדיוס השפעה. הוא מהיר, הוא זול, והוא לא יודע דבר על טיפוסים. הוא לא יכול לומר לך אם הקריאה שזה עתה כתבת מתקמפלת, אם השדה הזה קיים באובייקט, או אם שינוי השם שלך פספס ייצוא מחדש.

בלי שרת שפה, הסוכן מגלה זאת בשלב הבדיקות — או שלעולם לא. הוא כותב משהו סביר, ממשיך הלאה, והכשל צף שלושה תורים אחר כך, כשהוא כבר משויך לשינוי אחר.

אז הרווח הוא לא באמת כלי ה-lsp. הוא בכך ש-write, edit ו-apply_patch עטופים: אחרי כל כתיבה עובר הקובץ דרך שרת השפה, והודעות אבחון שהופיעו זה עתה מקופלות אל תוך תוצאת הכלי. הסוכן לומד שהוא שבר את הבנייה ברגע ששבר אותה, בין אם חשב לשאול ובין אם לא.

לחזור על מה שכבר נאמר לסוכן גרוע משתיקה: זה ממלא את התוצאה ברעש שהסוכן לומד לגלול על פניו, ובתוכו הולכת לאיבוד גם השגיאה החדשה היחידה. יומן נפרד לכל ריצה עוקב אחר מה שכבר דווח, לפי חומרה, קוד והודעה — לא לפי מיקום, כך שאבחון שפשוט ירד ארבע שורות למטה אינו מדווח מחדש כחדש.

עצלן, וישר בנוגע להיעדרותו

קטע בשם “עצלן, וישר בנוגע להיעדרותו”

אף שרת לא עולה עם האתחול. אחד עולה בבקשה הראשונה שזקוקה לו — כמעט תמיד העריכה הראשונה בקובץ שהוא תובע — והפעלות סרק מפונות. מארח שלעולם אינו נוגע ב-Dart לעולם לא מפעיל מנתח.

הזיהוי הוא חיתוך: סמן שורש חייב להתקיים ב-checkout וגם קובץ ההרצה חייב להיפתר. סמן לבדו אומר “הפרויקט הזה הוא Python” ושום דבר על כך שהשרת מותקן; קובץ הרצה שנפתר לבדו אומר שלמארח יש שרשרת כלים ושום דבר על ה-checkout הזה. רק שניהם יחד משמעם ששאילתה יכולה להצליח, וכלי שמוצע ותמיד נכשל גרוע מכלי שנעדר.

למה חיפוש מבני, כשיש grep

קטע בשם “למה חיפוש מבני, כשיש grep”

grep מתאים תווים. זה הכלי הנכון לשם והכלי הלא נכון לצורה.

תבנית מבנית מנותחת לפי אותו דקדוק של הקובץ, כך ש-dispose($X) מוצא את הקריאה בכל ריווח, שבירה או פיצול שורות, ולעולם אינו מתאים את אותם התווים בתוך מחרוזת ליטרלית או הערה.

התכונה שהופכת אותו ליותר מביטוי רגולרי עם כמה שלבים נוספים היא שמטא-משתנה שחוזר בתבנית אחת חייב ללכוד את אותו טקסט:

if ($X != null) $X.dispose()

מתאים לשומר שמשחרר את מה שבדק, ואינו מתאים לכזה שמשחרר משהו אחר. זהו סוג באג שאי אפשר לבטא טקסטואלית כלל.

הוא רץ על ה-tree-sitter שכבר מקושר

קטע בשם “הוא רץ על ה-tree-sitter שכבר מקושר”

אין מנוע שני. cc_natives כבר טוען את tree-sitter ואת הדקדוקים שלו עבור גרף הקוד, עם מטמון שאילתות מהודרות וטבלת שפות. המתאים הוא Dart טהור מעל עצים שממילא עברו ניתוח. מנוע מבני שני משמעו מנתח שני, סט דקדוקים שני שיש לבנות ולהכין בכל פלטפורמה, ושתי תשובות לשאלה “מהו ביטוי קריאה”.

לקטע קוד צריך להיות מקום לגור בו

קטע בשם “לקטע קוד צריך להיות מקום לגור בו”

דקדוק מגדיר רק מהו קובץ שלם. כשמנתחים אותו לבדו ברמה העליונה של Dart, dispose(x) הוא חתימת פונקציה תקפה לחלוטין — הכרזה בשם dispose שמקבלת פרמטר x — בלי שום שגיאה שתסגיר אותו. מתאים שנבנה על הניתוח הזה מחפש הכרזות, לא מוצא קריאות, ומדווח “אין התאמות” עבור תבנית שמעולם לא נקראה כפי שהיא נראית.

לכן תבנית נבחנת בסולם של פיגומים — הצהרה, ביטוי, איבר מחלקה, רמה עליונה — והראשון שמקבל אותה מנצח. קטע שאף הקשר לא מקבל מדווח כלא ניתן לניתוח, במקום לחפש אותו ולא למצוא אותו בשקט.

למה שכתוב עובר הכנה במקום להיכתב

קטע בשם “למה שכתוב עובר הכנה במקום להיכתב”

שכתוב מבני הוא העריכה הרחבה ביותר שסוכן יכול לבצע: תבנית אחת, ארבעים קבצים, ואף אחד לא קורא את התוצאה שורה אחר שורה — לא המודל, ולא האדם שביקש אותה. לכתוב אותה ולדווח “סיימתי” משמעו שההסתכלות הישרה הראשונה בשינוי תהיה git diff בדיעבד.

לכן ast_edit מדווח מה הוא היה עושה — המניין, הקבצים, האתרים המתאימים — והשינוי נוחת רק כש-resolve מחיל אותו. זה גם מה שמאפשר להציג הבדל ואת אישור/דחייה בפני אדם לפני שמשהו מגיע לדיסק.

ההחלה היא הכול או כלום, וכל קובץ נבדק מול התוכן שנלכד בזמן ההכנה. בין ההכנה להחלה, ייתכן שהסוכן ערך אחד מהקבצים האלה ביד, שמעבר אבחון כתב אותו מחדש, או שכלי עיצוב נגע בו — החלה בכל זאת תשליך את העבודה הזו, וההבדל ייראה מכוון. כל אי-התאמה דוחה את השינוי כולו, כי שכתוב מבני שהוחל חלקית משאיר עץ שאינו מתקמפל לפי אף אחת מהצורות.

מה מנפה עונה שאין הצהרת הדפסה יכולה

קטע בשם “מה מנפה עונה שאין הצהרת הדפסה יכולה”

לולאת הצהרות ההדפסה היא: מוסיפים הדפסה, מריצים את הכול מחדש, קוראים את הפלט, מוחקים את ההדפסה. היא עולה הרצת בדיקות מלאה לכל שאלה, והיא עונה רק על השאלה שחשבת לשאול לפני ההרצה.

מסגרת עצורה עונה בבת אחת על כל שאלה על אותו רגע — כל משתנה מקומי, כל קורא, וביטוי שמחושב בתחום של המסגרת עצמה. עבור תהליך תקוע או כשל שמשחזר פעם בעשר ריצות, זה ההבדל בין אחר צהריים לדקה.

חסום כמו מתחם, ומאותה סיבה

קטע בשם “חסום כמו מתחם, ומאותה סיבה”

מתאם ניפוי מחזיק בתהליך עצור שאוחז בכל מה שהתהליך הזה אוחז — פורט, מנעול, חיבור למסד נתונים. סשן שאיש אינו מפעיל הוא דליפה שמאריכה ימים מעבר לתור שהתחיל אותה. לכן יש TTL קשוח, בדיוק סשן אחד לכל שיחה, ו-launch שני נדחה במקום להחליף את הראשון בשקט: סוכן שמתחיל שני בלי לסיים את הראשון כמעט בוודאות איבד את הקשר לראשון.

תובלה אחת, שני פרוטוקולים

קטע בשם “תובלה אחת, שני פרוטוקולים”

Debug Adapter Protocol חולק עם Language Server Protocol את המסגור של Content-Length ושום דבר מעליו — מפתחות התאמה שונים, צורת כשל שונה, וסוג הודעה שלישי (אירועים) שאין לו מקבילה ב-LSP. המסגור חי במקום אחד; הפרוטוקולים יושבים עליו בנפרד.

כתיבת המסגור פעמיים פירושה שתי תשובות לשאלות “הודעה משתרעת על שני מקטעים”, “מקטע מכיל שלוש הודעות” ו“השרת כתב באנר ל-stdout לפני המסגרת הראשונה שלו”, ורק אחת מהן תקבל את התיקון.