Your Name commited on
Commit
d05b676
·
1 Parent(s): 63ddff9

Refactor math skills and update CSS styles; adjust tests for new skill identifiers

Browse files

- Updated math_skills.py to reflect changes in the teaching schedule and skill prerequisites.
- Modified CSS variables for improved styling consistency.
- Adjusted test_answer_browser.py and related test files to use new skill identifiers (e.g., a_30_side instead of g_triangle_area).
- Added documentation for the 9th-grade teaching schedule.

README.md CHANGED
@@ -10,7 +10,7 @@ pinned: false
10
 
11
  # Mathly
12
 
13
- Mathly is a Hebrew ninth-grade algebra and geometry practice path. It contains 59 skills across seven units, immediate feedback for known mistakes, and optional photographed-problem help.
14
 
15
  Choose **צילום בטלפון, פתרון במחשב** on the computer, scan the QR code with your phone, take a photo, and confirm sending it. The answer and worked solution appear automatically on the computer. Links last 15 minutes and accept one exercise; the phone does not need a separate login. Geometry practice includes diagrams for rectangle area, triangle area, and Pythagoras.
16
 
 
10
 
11
  # Mathly
12
 
13
+ Mathly is a Hebrew ninth-grade algebra and geometry practice path. It contains 140 skills across 14 units that follow the Ministry of Education's 9th-grade teaching schedule (see `docs/prisa_9A.md`), ten per unit, immediate feedback for known mistakes, and optional photographed-problem help.
14
 
15
  Choose **צילום בטלפון, פתרון במחשב** on the computer, scan the QR code with your phone, take a photo, and confirm sending it. The answer and worked solution appear automatically on the computer. Links last 15 minutes and accept one exercise; the phone does not need a separate login. Geometry practice includes diagrams for rectangle area, triangle area, and Pythagoras.
16
 
app/math_algebra9.json CHANGED
The diff for this file is too large to render. See raw diff
 
app/math_skills.py CHANGED
@@ -3,9 +3,9 @@
3
  Each skill names its prerequisites and a sample question. Choice samples identify mistakes;
4
  open-ended samples name their type and revision and accept a photographed answer.
5
  A mistake sends the student back to the skill it comes from (`back`), or keeps them on the same skill (`null`).
6
- The graph follows the Ministry of Education's 9th-grade teaching schedule (2026/27) and the summer review for
7
- students placed in 5 units; `core` marks what that review stresses. The questions are written for the app, and
8
- every skill, link and mistake still needs a 5-unit teacher's review.
9
 
10
  `problems` is run by the tests and on loading: a broken graph is refused before a student meets it.
11
  """
 
3
  Each skill names its prerequisites and a sample question. Choice samples identify mistakes;
4
  open-ended samples name their type and revision and accept a photographed answer.
5
  A mistake sends the student back to the skill it comes from (`back`), or keeps them on the same skill (`null`).
6
+ The units follow the Ministry of Education's 9th-grade teaching schedule for 2026/27 (`docs/prisa_9A.md`), with
7
+ ten skills in each; `month` is when the schedule teaches a skill. The questions are written for the app, and
8
+ every skill, link and mistake still needs a teacher's review.
9
 
10
  `problems` is run by the tests and on loading: a broken graph is refused before a student meets it.
11
  """
docs/prisa_9A.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div dir="rtl">
2
+
3
+ **משרד החינוך**
4
+ **מינהל חדשנות וטכנולוגיה**
5
+ **אגף א' STEM**
6
+ **הפיקוח על הוראת המתמטיקה**
7
+
8
+ # פריסת הוראה כיתה ט - תשפ"ז
9
+
10
+ ## הערות
11
+
12
+ 1. [מרחב פדגוגי - מתמטיקה חט"ב](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/) האתר המרכזי בו תמצאו [תוכניות ופריסות הוראה](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/tohnit-limudim/), [חומרי למידה](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/noseem_nilmadim/) בנושאים שונים בתכנית הלימודים, [מקבצי שאלות](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/question-files/) בנושאים שונים במרחב הפדגוגי, אוסף של [מאגרי שאלות ומבחני מפמ"ר](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/tests-exams/) ועוד.
13
+ 2. מומלץ לעיין גם [בתוכנית הלימודים המפורטת](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/tohnit-limudim/) כדי ללמוד על הדגשים ולראות דוגמאות מומלצות לתרגול.
14
+ 3. [אתר מוודל מתמטיקה חט"ב](https://sites.google.com/view/moodlemath/%D7%93%D7%A3-%D7%94%D7%91%D7%99%D7%AA)
15
+ 4. דגשים וחומרי למידה בחטיבה בעקבות תכנית הלימודים החדשה בחטיבה העליונה ניתן למצוא [במרחב הפדגוגי](https://pop.education.gov.il/tchumey_daat/matmatika/chativat-beynayim/teaching-mathematics/tohnit-limudim/) ובמסמך [הזה](https://docs.google.com/document/d/15jHBpFXm0knUWu-P02yoz5umW851wDXbfs3ExwZLwLw/edit?usp=drive_link).
16
+ 5. משימות אוריינות [תוכנית מאור](https://maor.haifa.ac.il/) - אוסף משימות לתלמידים בנושאים שונים בכיתות ז-ט ו[מדריך למורה](https://maor.haifa.ac.il/%D7%97%D7%95%D7%9E%D7%A8%D7%99%D7%9D-%D7%9C%D7%9E%D7%95%D7%A8%D7%99%D7%9D/).
17
+
18
+ ## דגשים חשובים לשנת הלימודים תשפ"ז
19
+
20
+ 1. **[הוראת גיאומטריה](https://docs.google.com/document/d/1poReJ2H5Q74HhPhoaSl-usaCuFOgzPkI/edit?usp=sharing)**: בהוראת הגיאומטריה יש להתאים את ההוראה עבור תלמידים המיועדים ללמוד ברמת 4 יח"ל: שאלות מדורגות, רמת סיבוכיות בינונית, שילוב של גיאומטריה במישור וגיאומטריה אנליטית ([מדריך למורה גיאומטריה בחט"ב](https://docs.google.com/document/d/1CFHKPpCDTMj3DDr5r2-t94sxYjSWNxLZxEEpFmx-MI0/edit?usp=sharing), [דגשים להוראת הגיאומטריה ברמת הביניים לתלמידי ט](https://docs.google.com/document/d/1AXiCbk5C_Xt8LzM0K7CmH1ZLjqH4swuS2ree1fRzO6c/edit?usp=sharing)).
21
+ מומלץ להיעזר בקובץ [גיאומטריה, מספריים וקיפולי נייר](https://drive.google.com/file/d/1eQhe4y1l3bPYOYwjnX6_jPQG77f6mo3z/view?usp=drive_link) (המחשת התכונות על ידי נגזרות מנייר וקיפולי נייר).
22
+ 2. שאלות אוריינות לשלב בכל נושאי הלימוד בהתאם לפריסה. אוסף דוגמאות - לחצו [כאן](https://meyda.education.gov.il/files/Pop/0files/matmatika/Chativat-Beynayim/tashpah/orianut9A-3th.pdf).
23
+ 3. הנושא של טכניקה אלגברית הכולל את הנושאים חוק הפילוג המורחב ושברים אלגבריים יילמד בתחילת כיתה ט. הנושא 'חוקי חזקות' יילמד בשליש השלישי של כיתה ט.
24
+ 4. הנושא משולש ישר זווית שזוויותיו 30°, 60°, 90° יילמד בכיתה ט (הנחיות בפריסה).
25
+ 5. הנושאים **שלא ילמדו בשנת הלימודים תשפ"ז** ברמת לימוד זו: קטע אמצעים במשולש ובטרפז.
26
+
27
+ ---
28
+
29
+ ## פריסה לפי חודשים
30
+
31
+ ### ספטמבר - 10 שעות
32
+
33
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
34
+ |---|---|---|---|---|
35
+ | תחום אלגברי | 6 | טכניקה אלגברית<br>[רצף הוראה, דגשים ודוגמאות](https://docs.google.com/document/d/19e3cNxPUSn_8_kFlvdA5kPaEFZP5bETPGEoyZ_3cPcQ/edit?usp=sharing)<br>[חומרי הוראה](https://docs.google.com/document/d/1r0w_3OfQeNhSGjqtq0KAAoIBCHOg8GiEnXX9MP_BWNg/edit?usp=sharing) | חוק הפילוג המורחב, פישוט ביטויים בעזרת חוק הפילוג המורחב, פתרון משוואות בעזרת חוק הפילוג המורחב<br>שאלות השוואת שטחים בעזרת חוק הפילוג<br>נוסחאות הכפל המקוצר | יש לפתח את נוסחאות הכפל באמצעים אלגבריים, ולהדגים אותן באמצעים גיאומטריים<br>**(יש להכיר את חוקי החזקות ברמה בסיסית אך ורק לצורך הטכניקה האלגברית הנלמדת, מתוך הבנת הפעולה ככפל חוזר. בהמשך השנה תהיה העמקה בנושא).** |
36
+ | תחום גיאומטרי | 4 | דלתון<br>[חומרי למידה גיאומטריה](https://docs.google.com/document/d/11jLweRSVFDczh8xqdkpDiREhy8nslzt92BJ5qyZEliI/edit?usp=sharing) | דלתון: הכרת הדלתון, זוויות בדלתון, האלכסון הראשי בדלתון | לשלב דלתון במערכת צירים |
37
+
38
+ ### אוקטובר - 20 שעות
39
+
40
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
41
+ |---|---|---|---|---|
42
+ | תחום אלגברי | 12 | טכניקה אלגברית - המשך | פירוק לגורמים בעזרת הוצאת גורם משותף, פירוק של תלת-איבר ריבועי (טרינום ריבועי כאשר המקדם 1) ונוסחאות הכפל המקוצר<br>פתרון משוואות ריבועיות בסיסיות באמצעות פירוק לגורמים<br>צמצום שברים אלגבריים<br>כפל וחילוק שברים אלגבריים | |
43
+ | תחום גיאומטרי | 8 | משולש שווה שוקיים<br>[חומרי למידה גיאומטריה](https://docs.google.com/document/d/11jLweRSVFDczh8xqdkpDiREhy8nslzt92BJ5qyZEliI/edit?usp=sharing) | משפטים הפוכים להוכחה כי משולש הוא שווה שוקיים. | בכיתה ח התלמידים למדו את המשפט "במשולש שווה שוקיים חוצה זווית הראש, הגובה לבסיס והתיכון לבסיס מתלכדים". בכיתה ט ללמד משפטים הפוכים.<br>לשלב משולש שווה שוקיים במערכת צירים. |
44
+
45
+ ### נובמבר - 22 שעות
46
+
47
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
48
+ |---|---|---|---|---|
49
+ | תחום אלגברי | 14 | פונקציות ריבועיות<br>פתרון משוואות ריבועיות<br>[הצעת הוראה מפורטת פונקציה ריבועית](https://docs.google.com/document/d/1vegBqeqkV-4fWZd-R-b98BE4AeMqZNvGGe0AuY-RCiI/edit?usp=sharing)<br>[חומרי הוראה](https://docs.google.com/document/d/1UO8fk2dvhLfO_30QybmXMTzNRYSksD9i73t1X_S7zoA/edit?usp=sharing) | - היכרות עם הגרף והביטוי האלגברי של הפונקציה $f(x) = ax^2$ כאשר $a \neq 0$ (משמעות ערכו של a רק לצורך קביעת סוג הקיצון ולא כיווץ ומתיחה)<br>- תכונות הפונקציה הריבועית (התייחסות איכותנית ללא תבנית כאשר נתון גרף של פונקציה ריבועית): נקודת קיצון, עלייה וירידה, נקודות חיתוך עם הצירים, חיוביות ושליליות<br>- הזזה אנכית של פונקציה ריבועית $f(x) = ax^2 + k$<br>- הזזה אופקית של פונקציה ריבועית $f(x) = a(x - b)^2$<br>- שילוב הזזה אופקית ואנכית $f(x) = a(x - b)^2 + k$<br>- הצגת פונקציה ריבועית כמכפלה<br>- הצגת פונקציה ריבועית בהצגה סטנדרטית<br>- פתרון משוואות ריבועיות בעזרת נוסחת שורשים (שאפשר להציג בייצוג סטנדרטי $ax^2 + bx + c = 0$ כאשר $a \neq 0$)<br>- סרטוט גרף של פונקציה ריבועית על סמך ייצוג סטנדרטי | אפשר להתחיל את נושא הפונקציות בכיתה ט מתכונות של פונקציה ולהשלים את הנדרש בפונקציה ריבועית.<br>הסברים ניתן למצוא בקובץ [גישות שונות להוראת פונקציות בכיתה ט](https://docs.google.com/document/d/1JcrG2JM-L1BOGAEN_8XN-vidnvlAxrewF8rpfuT7ISc/edit?usp=sharing)<br>**אין צורך במעבר בין הייצוגים השונים של פונקציה ריבועית. יש לפתור משוואה ריבועית במגוון דרכים ולעודד לזהות דרכי פתרון יעילות למשוואה נתונה על פי אופן ייצוגה.** |
50
+ | תחום גיאומטרי | 2 | יחסי צלעות וזוויות | משפטים:<br>- אם במשולש שתי צלעות שונות זו מזו, אז מול הצלע הגדולה מביניהן ממוקמת הזווית הגדולה.<br>- אם במשולש שתי זוויות שונות זו מזו, אז מול הזווית הגדולה מביניהן ממוקמת הצלע הגדולה. | |
51
+ | תחום גיאומטרי | 2 | משולש ישר זווית 30°, 60°, 90° | משפט - במשולש ישר זווית שאחת מזוויותיו בת 30°, הניצב מול זווית זו שווה באורכו למחצית היתר.<br>משפט הפוך - משולש ישר זווית שבו הניצב שווה למחצית מהיתר, הזווית שמול ניצב זה בת 30°. | יש ללמד משפט זה במסגרת לימוד משולש שווה צלעות. אפשר להוריד גובה/ תיכון/ חוצה זווית מאחד הקודקודים ולדון בתוצאות שהתקבלו. |
52
+ | תחום גיאומטרי | 4 | ישרים מקבילים, טרפז | תכונות של הקבלה או אי הקבלה<br>ישרים מקבילים משפט הפוך (התנאים שצריכים להתקיים על מנת ששני ישרים הנחתכים ע"י ישר שלישי יהיו מקבילים).<br>זוויות חד צדדיות בין ישרים מקבילים (לא חובה)<br>הכרת הטרפז<br>טרפז שווה שוקיים<br>הוכחה שמרובע הוא טרפז<br>שטח והיקף טרפז | |
53
+
54
+ ### דצמבר - 15 שעות
55
+
56
+ [מסכמים מחצית](https://docs.google.com/document/d/19L9H2ZhtCTeI2EfO1BcG26XgRJBpAUkNjhgNoJwPY14/edit?usp=sharing), [ערבית](https://docs.google.com/document/d/1WlpXEH400N85t8iV5zanOW9sx5U6ZlFXkcGKPz4A8es/edit?usp=sharing)
57
+
58
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
59
+ |---|---|---|---|---|
60
+ | תחום אלגברי | 6 | המשך פתרון משוואות ריבועיות ויישומים | כולל שימוש בטכניקה אלגברית | |
61
+ | תחום אלגברי | 3 | אי שוויונות ריבועיים | פתרון אי שוויון ריבועי בהקשר של פונקציה ריבועית | |
62
+ | תחום גיאומטרי | 2 | המשך ישרים מקבילים וטרפז | | לשלב טרפז במערכת צירים |
63
+ | תחום גיאומטרי | 4 | מקבילית | הגדרת מקבילית<br>תכונות צלעות, זוויות ואלכסונים<br>משפטים הפוכים | לשלב מקבילית במערכת צירים |
64
+
65
+ ### ינואר - 21 שעות
66
+
67
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
68
+ |---|---|---|---|---|
69
+ | תחום אלגברי | 8 | משוואות רציונליות | פתרון משוואות עם פירוק לגורמים במכנה, תחום הצבה (לרבות משוואות שבהן אחד הפתרונות אינו קביל) | |
70
+ | תחום גיאומטרי | 6 | מקבילית - המשך | הגדרת מקבילית<br>תכונות צלעות, זוויות ואלכסונים<br>משפטים הפוכים | |
71
+ | תחום גיאומטרי | 2 | מלבן | הגדרה, תכונות צלעות, זוויות, אלכסונים<br>משפטים הפוכים | |
72
+ | תחום אי ודאות | 3 | הסתברות<br>[חומרי למידה](https://docs.google.com/document/d/13bl0d_wGjFSCChXxi7tkSe332M4IkidjqwmQanCzUV4/edit?usp=sharing) | מהי הסתברות, מושגים בסיסיים (כולל השלמה של כיתה ח), חישובי הסתברות והקשר לשכיחות יחסית, מאורע משלים, מאורעות דו שלביים, ייצוגים שונים (טבלה, דיאגרמת עץ, מודל השטח) | לשלב בתרגול חזרה על סטטיסטיקה |
73
+ | תחום אלגברי וגיאומטרי | 2 | | משימות אורייניות אינטגרטיביות | |
74
+
75
+ ### פברואר - 20 שעות
76
+
77
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
78
+ |---|---|---|---|---|
79
+ | תחום אלגברי | 6 | מערכת משוואות ממעלה שנייה | משמעות אלגברית ומשמעות גרפים של פתרון מערכת משוואות | לקשר בין פתרון מערכת המשוואות לייצוגים גרפיים (חיתוך בין שתי פרבולות, חיתוך בין פרבולה לישר) |
80
+ | תחום אי ודאות | 10 | הסתברות - המשך | | |
81
+ | תחום גיאומטרי | 4 | מלבן - המשך | | |
82
+
83
+ ### מרץ - 18 שעות
84
+
85
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
86
+ |---|---|---|---|---|
87
+ | תחום אלגברי | 12 | תכונות של פונקציה<br>[חומרי למידה](https://docs.google.com/document/d/1WI9C2OnSaa85TDvpnfjjYho9YY3QvyeBM9AcARnh0hU/edit?usp=sharing) | [רצף הוראה תכונות של פונקציה](https://docs.google.com/document/d/1UFK9cz32wId_Ntb3w5R5fEm42FjuK5gr/edit?usp=sharing)<br>1. תכונות של גרף: נקודות חיתוך עם הצירים, תחומי החיוביות והשליליות, נקודות קיצון, תחומי עליה וירידה<br>2. הקשר בין משוואות לגרף של פונקציה<br>3. הזזה אנכית, הזזה אופקית<br>4. שיקוף ביחס לציר ה-x | אפשר להתחיל את נושא הפונקציות בכיתה ט' מתכונות של פונקציה ולהשלים את הנדרש בפונקציה ריבועית.<br>הסברים ניתן למצוא בקובץ [גישות שונות להוראת פונקציות בכיתה ט'](https://docs.google.com/document/d/1JcrG2JM-L1BOGAEN_8XN-vidnvlAxrewF8rpfuT7ISc/edit?usp=sharing)<br>1. יש להדגיש את ההבדל בין נקודות קיצון בסביבה מסוימת לנקודות קיצון מוחלט (רק דרך גרף)<br>2. יש להדגיש כי שיעורי נקודת קיצון הם זוג סדור.<br>3. יש להדגיש כי ערך המינימום/מקסימום הוא שיעור ה-Y של נקודת הקיצון.<br>4. יש להראות מקרים בהם אין כלל נקודות קיצון<br>5. סרטוט גרף של פונקציה על פי התכונות הנתונות<br>6. יש להבחין בין הזזה אנכית מטה ומעלה<br>7. יש לדון בתכונות הפונקציה כאשר היא מוזזת (איזה תכונות נשמרות ואיזה אינן נשמרות)<br>8. יש לתרגל סרטוט פונקציה ופונקציה מוזזת באותה מערכת צירים.<br>9. יש לשים לב כי בהזזה אופקית תלמידים נוטים להזיז ימינה כאשר a>0. |
88
+ | תחום גיאומטרי | 4 | מעוין | הגדרה, תכונות הצלעות<br>תכונות הזוויות<br>תכונות האלכסונים משפטים הפוכים | |
89
+ | תחום גיאומטרי | 2 | ריבוע | הגדרה, תכונות הצלעות<br>תכונות הזוויות<br>תכונות האלכסונים משפטים הפוכים | |
90
+
91
+ ### אפריל - 5 שעות
92
+
93
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
94
+ |---|---|---|---|---|
95
+ | תחום גיאומטרי | 2 | ריבוע - המשך | | |
96
+ | תחום גיאומטרי | 3 | משולש ישר זווית | התיכון ליתר<br>תרגול המשפט בשילוב מרובעים | |
97
+
98
+ ### מאי - 19 שעות
99
+
100
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
101
+ |---|---|---|---|---|
102
+ | תחום אלגברי | 5 | חוקי חזקות | לימוד בסיסי של החוקים: חוקי חזקות מעריך טבעי או אפס<br>חוקי שורש ריבועי | לא ללמד חזקות עם מעריך שלילי<br>לשלב שאלות אוריינות |
103
+ | תחום אלגברי | 10 | שאלות מילוליות<br>[חומרי למידה](https://docs.google.com/document/d/1mScGiBhYk3nSmy5R7XjsdjQZbkxT_8NBo2b3ZXwWkHM/edit?usp=sharing) | שאלות מילוליות (לרבות שימוש במשוואות ריבועית או מערכת משוואות ממעלה שנייה) הנדסיות וכלליות.<br>שאלות תנועה.<br>שילוב אחוזים.<br>כולל שאלות אוריינות | |
104
+ | תחום גיאומטרי | 4 | דמיון משולשים<br>[חומרי למידה](https://docs.google.com/document/d/1qdTPJlggOo59At6ertM6YqNoLc1aw0GOOz-ZCSt8Dq8/edit?usp=sharing) | יחסי שטחים במשולשים דומים | |
105
+
106
+ ### יוני - 12 שעות
107
+
108
+ | תחום לימודי | שעות | הנושא | פירוט הנושא | הערות |
109
+ |---|---|---|---|---|
110
+ | | 12 | השלמות וחזרות<br>[חומרי למידה](https://docs.google.com/document/d/19L9H2ZhtCTeI2EfO1BcG26XgRJBpAUkNjhgNoJwPY14/edit?usp=sharing), [ערבית](https://docs.google.com/document/d/1WlpXEH400N85t8iV5zanOW9sx5U6ZlFXkcGKPz4A8es/edit?usp=sharing) | | |
111
+
112
+ </div>
static/math.css CHANGED
@@ -6,7 +6,8 @@
6
  --ok:#6CC08A; --ok-bg:#12291B; --sumac:#F08A96; --sumac-bg:#33161B; --amber:#E0B45A; --amber-bg:#2E2512;
7
  --olive:#A6C77A; --olive-soft:#1E2618;
8
  --mx-accent:#7FA6F0; --mx-accent-soft:#18243A; --mx-on-accent:#06101F;
9
- --u-f:#A3AD9F; --u-t:#93B85F; --u-n:#5CC2B2; --u-q:#6FB3D2; --u-p:#E0B45A; --u-w:#D98AAE;
 
10
  --mx-scrim:rgb(0 0 0 / .7);
11
  --f-math:"STIX Two Text","Cambria Math","Times New Roman",serif;
12
  }
@@ -15,8 +16,10 @@ html.math-on, html.math-on body{overflow:hidden}
15
  padding-block:max(12px, env(safe-area-inset-top, 0px)) calc(32px + env(safe-area-inset-bottom, 0px));
16
  padding-left:max(16px, env(safe-area-inset-left, 0px));padding-right:max(16px, env(safe-area-inset-right, 0px))}
17
  #mathView[hidden]{display:none}
18
- #mathView .u-f{--uc:var(--u-f)} #mathView .u-t{--uc:var(--u-t)} #mathView .u-n{--uc:var(--u-n)}
19
- #mathView .u-q{--uc:var(--u-q)} #mathView .u-p{--uc:var(--u-p)} #mathView .u-w{--uc:var(--u-w)}
 
 
20
  #mathView h1,#mathView h2,#mathView h3{font-family:var(--f-display);font-weight:400;margin:0;text-wrap:balance}
21
  #mathView button{font-family:var(--f-body)}
22
  #mathView button:focus-visible{outline:3px solid var(--mx-accent);outline-offset:2px}
@@ -134,7 +137,6 @@ html.math-on, html.math-on body{overflow:hidden}
134
  .mx-work{margin-top:6px;padding:8px 10px;border-radius:10px;background:var(--mx-accent-soft)}
135
  .mx-row{display:flex;gap:8px;flex-wrap:wrap}
136
  @media (prefers-reduced-motion:no-preference){.mx-sheet{animation:mx-up .22s ease-out}@keyframes mx-up{from{transform:translateY(30px);opacity:.4}}}
137
- .u-g{--uc:#B89BEB}
138
  .mx-geometry{direction:ltr;align-self:center;width:100%;max-width:320px;text-align:center}
139
  .mx-geometry svg{display:block;width:100%;height:auto}
140
  .mx-geometry path{stroke:var(--uc);stroke-width:2;fill:none}
 
6
  --ok:#6CC08A; --ok-bg:#12291B; --sumac:#F08A96; --sumac-bg:#33161B; --amber:#E0B45A; --amber-bg:#2E2512;
7
  --olive:#A6C77A; --olive-soft:#1E2618;
8
  --mx-accent:#7FA6F0; --mx-accent-soft:#18243A; --mx-on-accent:#06101F;
9
+ --u-t:#93B85F; --u-k:#B89BEB; --u-q:#6FB3D2; --u-e:#7C9BE6; --u-a:#C99BD8; --u-z:#9C8FD6; --u-r:#C8C26A;
10
+ --u-h:#E0915A; --u-s:#5CA8C2; --u-m:#D6A3E0; --u-n:#5CC2B2; --u-p:#E0B45A; --u-w:#D98AAE; --u-d:#A9B7F2;
11
  --mx-scrim:rgb(0 0 0 / .7);
12
  --f-math:"STIX Two Text","Cambria Math","Times New Roman",serif;
13
  }
 
16
  padding-block:max(12px, env(safe-area-inset-top, 0px)) calc(32px + env(safe-area-inset-bottom, 0px));
17
  padding-left:max(16px, env(safe-area-inset-left, 0px));padding-right:max(16px, env(safe-area-inset-right, 0px))}
18
  #mathView[hidden]{display:none}
19
+ #mathView .u-t{--uc:var(--u-t)} #mathView .u-k{--uc:var(--u-k)} #mathView .u-q{--uc:var(--u-q)} #mathView .u-e{--uc:var(--u-e)}
20
+ #mathView .u-a{--uc:var(--u-a)} #mathView .u-z{--uc:var(--u-z)} #mathView .u-r{--uc:var(--u-r)} #mathView .u-h{--uc:var(--u-h)}
21
+ #mathView .u-s{--uc:var(--u-s)} #mathView .u-m{--uc:var(--u-m)} #mathView .u-n{--uc:var(--u-n)} #mathView .u-p{--uc:var(--u-p)}
22
+ #mathView .u-w{--uc:var(--u-w)} #mathView .u-d{--uc:var(--u-d)}
23
  #mathView h1,#mathView h2,#mathView h3{font-family:var(--f-display);font-weight:400;margin:0;text-wrap:balance}
24
  #mathView button{font-family:var(--f-body)}
25
  #mathView button:focus-visible{outline:3px solid var(--mx-accent);outline-offset:2px}
 
137
  .mx-work{margin-top:6px;padding:8px 10px;border-radius:10px;background:var(--mx-accent-soft)}
138
  .mx-row{display:flex;gap:8px;flex-wrap:wrap}
139
  @media (prefers-reduced-motion:no-preference){.mx-sheet{animation:mx-up .22s ease-out}@keyframes mx-up{from{transform:translateY(30px);opacity:.4}}}
 
140
  .mx-geometry{direction:ltr;align-self:center;width:100%;max-width:320px;text-align:center}
141
  .mx-geometry svg{display:block;width:100%;height:auto}
142
  .mx-geometry path{stroke:var(--uc);stroke-width:2;fill:none}
tests/test_answer_browser.py CHANGED
@@ -48,7 +48,7 @@ def session(browser, owner=OWNER, mobile=False):
48
 
49
 
50
  def open_question(page):
51
- page.locator('.mx-node[data-id="g_triangle_area"]').click()
52
  playwright.expect(page.locator('.mx-answer')).to_be_visible()
53
  playwright.expect(page.locator('.mx-choices')).to_have_count(0)
54
 
@@ -109,7 +109,7 @@ def test_two_device_capture_retake_failure_retry_and_original_attempt(scan_serve
109
  phone.locator('[data-submit]').click()
110
  playwright.expect(computer.locator('.mx-answer [role=status]')).to_have_text('מעלים את התשובה…', timeout=10000)
111
  assert started.is_set()
112
- computer.locator('.mx-x').click();computer.locator('.mx-node[data-id="g_rectangle"]').click()
113
  release.set()
114
  playwright.expect(phone.locator('[data-status]')).to_contain_text('הועלתה בהצלחה', timeout=10000)
115
  assert computer.locator('.mx-answer-image').count() == 0
@@ -118,7 +118,7 @@ def test_two_device_capture_retake_failure_retry_and_original_attempt(scan_serve
118
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
119
  assert computer.locator('.mx-answer-image').evaluate('img=>img.complete&&img.naturalWidth>0')
120
  computer.screenshot(path='/tmp/mathly-answer-computer.png', full_page=True, animations='disabled')
121
- assert computer.evaluate("!JSON.parse(localStorage.getItem('mathly.progress.v1:'+'%s@test.invalid')||'{}').done?.g_triangle_area" % OWNER)
122
  computer.reload();open_question(computer)
123
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
124
  foreign = session(browser, OTHER, mobile=True).new_page();foreign.goto(url)
@@ -155,7 +155,7 @@ def test_expiry_renewal_reconnect_and_account_change(scan_server):
155
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible(timeout=10000)
156
  # Account change removes the photograph and all old-account continuations.
157
  computer.evaluate("localStorage.setItem('test.account','%s');window.testAuthEvent('SIGNED_IN',{access_token:'%s',user:{id:'%s'}})" % (OTHER, OTHER, OTHER))
158
- playwright.expect(computer.locator('.mx-node[data-id="g_triangle_area"]')).to_be_visible()
159
  assert computer.locator('.mx-answer-image').count() == 0
160
  open_question(computer)
161
  playwright.expect(computer.locator('[data-request]')).to_be_enabled()
@@ -187,9 +187,9 @@ def test_success_response_loss_is_safe_to_retry(scan_server):
187
  playwright.expect(phone.locator('[data-status]')).to_contain_text('הועלתה בהצלחה')
188
  with store.connect() as c:
189
  assert c.execute('select count(*) from math_answer_attachments').fetchone()[0]==1
190
- computer.locator('.mx-x').click();computer.locator('.mx-node[data-id="g_rectangle"]').click()
191
  computer.locator('.mx-choices button[data-m=""]').click()
192
- assert computer.evaluate("!!JSON.parse(localStorage.getItem('mathly.progress.v1:'+'%s@test.invalid')).done.g_rectangle" % OWNER)
193
  finally:
194
  browser.close()
195
 
@@ -216,7 +216,7 @@ def test_check_feedback_progress_reload_and_retake(scan_server, monkeypatch, ver
216
  monkeypatch.setattr('app.answer_routes.AnswerGrader', Grader)
217
  from test_answer_scans import begin, finish
218
  from app.math_skills import algebra9, question_version
219
- skill = next(s for s in algebra9()['skills'] if s['id'] == 'g_triangle_area')
220
  done = store.action(OWNER, 'resume', question_id=skill['id'],
221
  question_version=question_version(skill['sample']), snapshot=skill['sample'])
222
  done = finish(store, begin(store, store.action(OWNER, 'create_scan', attempt_id=done['attempt']['id'])))
@@ -241,7 +241,7 @@ def test_check_feedback_progress_reload_and_retake(scan_server, monkeypatch, ver
241
  if verdict in ('unreadable', 'needs_review'):
242
  assert '/100' not in panel.inner_text()
243
  key = 'mathly.progress.v1:' + OWNER + '@test.invalid'
244
- assert computer.evaluate("key=>!!JSON.parse(localStorage.getItem(key)||'{}').done?.g_triangle_area", key) == (verdict == 'correct')
245
  computer.reload();open_question(computer)
246
  playwright.expect(computer.locator('.mx-grade')).to_be_visible()
247
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
 
48
 
49
 
50
  def open_question(page):
51
+ page.locator('.mx-node[data-id="a_30_side"]').click()
52
  playwright.expect(page.locator('.mx-answer')).to_be_visible()
53
  playwright.expect(page.locator('.mx-choices')).to_have_count(0)
54
 
 
109
  phone.locator('[data-submit]').click()
110
  playwright.expect(computer.locator('.mx-answer [role=status]')).to_have_text('מעלים את התשובה…', timeout=10000)
111
  assert started.is_set()
112
+ computer.locator('.mx-x').click();computer.locator('.mx-node[data-id="m_rect_area"]').click()
113
  release.set()
114
  playwright.expect(phone.locator('[data-status]')).to_contain_text('הועלתה בהצלחה', timeout=10000)
115
  assert computer.locator('.mx-answer-image').count() == 0
 
118
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
119
  assert computer.locator('.mx-answer-image').evaluate('img=>img.complete&&img.naturalWidth>0')
120
  computer.screenshot(path='/tmp/mathly-answer-computer.png', full_page=True, animations='disabled')
121
+ assert computer.evaluate("!JSON.parse(localStorage.getItem('mathly.progress.v1:'+'%s@test.invalid')||'{}').done?.a_30_side" % OWNER)
122
  computer.reload();open_question(computer)
123
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
124
  foreign = session(browser, OTHER, mobile=True).new_page();foreign.goto(url)
 
155
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible(timeout=10000)
156
  # Account change removes the photograph and all old-account continuations.
157
  computer.evaluate("localStorage.setItem('test.account','%s');window.testAuthEvent('SIGNED_IN',{access_token:'%s',user:{id:'%s'}})" % (OTHER, OTHER, OTHER))
158
+ playwright.expect(computer.locator('.mx-node[data-id="a_30_side"]')).to_be_visible()
159
  assert computer.locator('.mx-answer-image').count() == 0
160
  open_question(computer)
161
  playwright.expect(computer.locator('[data-request]')).to_be_enabled()
 
187
  playwright.expect(phone.locator('[data-status]')).to_contain_text('הועלתה בהצלחה')
188
  with store.connect() as c:
189
  assert c.execute('select count(*) from math_answer_attachments').fetchone()[0]==1
190
+ computer.locator('.mx-x').click();computer.locator('.mx-node[data-id="m_rect_area"]').click()
191
  computer.locator('.mx-choices button[data-m=""]').click()
192
+ assert computer.evaluate("!!JSON.parse(localStorage.getItem('mathly.progress.v1:'+'%s@test.invalid')).done.m_rect_area" % OWNER)
193
  finally:
194
  browser.close()
195
 
 
216
  monkeypatch.setattr('app.answer_routes.AnswerGrader', Grader)
217
  from test_answer_scans import begin, finish
218
  from app.math_skills import algebra9, question_version
219
+ skill = next(s for s in algebra9()['skills'] if s['id'] == 'a_30_side')
220
  done = store.action(OWNER, 'resume', question_id=skill['id'],
221
  question_version=question_version(skill['sample']), snapshot=skill['sample'])
222
  done = finish(store, begin(store, store.action(OWNER, 'create_scan', attempt_id=done['attempt']['id'])))
 
241
  if verdict in ('unreadable', 'needs_review'):
242
  assert '/100' not in panel.inner_text()
243
  key = 'mathly.progress.v1:' + OWNER + '@test.invalid'
244
+ assert computer.evaluate("key=>!!JSON.parse(localStorage.getItem(key)||'{}').done?.a_30_side", key) == (verdict == 'correct')
245
  computer.reload();open_question(computer)
246
  playwright.expect(computer.locator('.mx-grade')).to_be_visible()
247
  playwright.expect(computer.locator('.mx-answer-image')).to_be_visible()
tests/test_answer_scans.py CHANGED
@@ -237,11 +237,11 @@ def test_enable_rollback_preserves_received_answers(answer_store):
237
  async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app),base_url='http://test',headers={'Authorization':f'Bearer {OWNER}'}) as c:
238
  assert (await c.get('/api/me')).json()['answer_scans'] is False
239
  skills=(await c.get('/api/math/skills')).json()['skills']
240
- assert len(next(s for s in skills if s['id']=='g_triangle_area')['sample']['choices'])==3
241
  assert (await c.get('/api/math/answer-scans')).status_code==503
242
  store.enabled=True
243
  skills=(await c.get('/api/math/skills')).json()['skills']
244
- assert next(s for s in skills if s['id']=='g_triangle_area')['sample']['type']=='open-ended'
245
  assert (await c.get(f"/api/math/answer-attempts/{done['attempt']['id']}/image")).status_code==200
246
  with store.connect() as c:
247
  assert c.execute('select count(*) from classes').fetchone()[0]==1
 
237
  async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app),base_url='http://test',headers={'Authorization':f'Bearer {OWNER}'}) as c:
238
  assert (await c.get('/api/me')).json()['answer_scans'] is False
239
  skills=(await c.get('/api/math/skills')).json()['skills']
240
+ assert len(next(s for s in skills if s['id']=='a_30_side')['sample']['choices'])==3
241
  assert (await c.get('/api/math/answer-scans')).status_code==503
242
  store.enabled=True
243
  skills=(await c.get('/api/math/skills')).json()['skills']
244
+ assert next(s for s in skills if s['id']=='a_30_side')['sample']['type']=='open-ended'
245
  assert (await c.get(f"/api/math/answer-attempts/{done['attempt']['id']}/image")).status_code==200
246
  with store.connect() as c:
247
  assert c.execute('select count(*) from classes').fetchone()[0]==1
tests/test_math_skills.py CHANGED
@@ -33,14 +33,14 @@ def test_choice_samples_still_require_one_correct_choice():
33
  def test_the_algebra_graph_is_whole():
34
  graph = algebra9()
35
  assert problems(graph) == []
36
- assert len(graph["skills"]) == 59
37
- assert {unit["id"] for unit in graph["units"]} == {"f", "t", "n", "q", "p", "w", "g"}
38
 
39
 
40
  def test_a_broken_graph_is_named():
41
  graph = copy.deepcopy(algebra9())
42
  skill = lambda sid: next(item for item in graph["skills"] if item["id"] == sid)
43
- skill("f_neg")["prereqs"] = ["f_ops"]
44
  assert problems(graph)[0].startswith("cycle:")
45
 
46
 
@@ -57,7 +57,7 @@ def test_math_is_the_only_page_and_api():
57
  class FakeSolver:
58
  def solve(self, image, media_type):
59
  return {"readable": True, "problem": "$x² = 9$", "steps": [{"hint": "h", "work": "w"}],
60
- "answer": "$x = ±3$", "skill": "q_eq_simple", "note": ""}
61
 
62
 
63
  def test_a_photographed_problem_is_solved(monkeypatch):
@@ -76,8 +76,8 @@ def test_a_photographed_problem_is_solved(monkeypatch):
76
  def test_rollout_and_rollback_question_variants():
77
  enabled, disabled = practice_graph(True), practice_graph(False)
78
  assert problems(enabled) == problems(disabled) == []
79
- opened = next(s for s in enabled['skills'] if s['id']=='g_triangle_area')
80
- fallback = next(s for s in disabled['skills'] if s['id']=='g_triangle_area')
81
  assert opened['sample']['type']=='open-ended' and 'choices' not in opened['sample']
82
  assert opened['question_version']==question_version(opened['sample'])
83
  assert len(fallback['sample']['choices']) == 3
 
33
  def test_the_algebra_graph_is_whole():
34
  graph = algebra9()
35
  assert problems(graph) == []
36
+ assert len(graph["skills"]) == 140
37
+ assert {unit["id"] for unit in graph["units"]} == set("tkqearzhsmnpwd")
38
 
39
 
40
  def test_a_broken_graph_is_named():
41
  graph = copy.deepcopy(algebra9())
42
  skill = lambda sid: next(item for item in graph["skills"] if item["id"] == sid)
43
+ skill("t_ext")["prereqs"] = ["t_square"]
44
  assert problems(graph)[0].startswith("cycle:")
45
 
46
 
 
57
  class FakeSolver:
58
  def solve(self, image, media_type):
59
  return {"readable": True, "problem": "$x² = 9$", "steps": [{"hint": "h", "work": "w"}],
60
+ "answer": "$x = ±3$", "skill": "e_simple", "note": ""}
61
 
62
 
63
  def test_a_photographed_problem_is_solved(monkeypatch):
 
76
  def test_rollout_and_rollback_question_variants():
77
  enabled, disabled = practice_graph(True), practice_graph(False)
78
  assert problems(enabled) == problems(disabled) == []
79
+ opened = next(s for s in enabled['skills'] if s['id']=='a_30_side')
80
+ fallback = next(s for s in disabled['skills'] if s['id']=='a_30_side')
81
  assert opened['sample']['type']=='open-ended' and 'choices' not in opened['sample']
82
  assert opened['question_version']==question_version(opened['sample'])
83
  assert len(fallback['sample']['choices']) == 3
tests/test_phone_browser.py CHANGED
@@ -102,7 +102,7 @@ def test_phone_refresh_upload_and_desktop_network_recovery(phone_server):
102
  def test_class_management_geometry_and_permissions(phone_server, manager):
103
  import json
104
 
105
- row = {"id": 7, "name": "כיתת תרגול", "lessons": ["f"],
106
  "emails": ["student@example.com"], "teachers": ["teacher@example.com"]}
107
  writes = []
108
  with playwright.sync_playwright() as p:
@@ -144,7 +144,7 @@ def test_class_management_geometry_and_permissions(phone_server, manager):
144
  playwright.expect(button).to_have_count(0)
145
  return
146
  button.click()
147
- geometry = page.locator('input[name="unit"][value="g"]')
148
  geometry.check()
149
  page.get_by_role("button", name="שמירת השינויים").click()
150
  playwright.expect(page.get_by_role("status")).to_contain_text("לא הצלחנו לשמור")
@@ -152,10 +152,10 @@ def test_class_management_geometry_and_permissions(phone_server, manager):
152
  page.get_by_role("button", name="שמירת השינויים").click()
153
  playwright.expect(page.get_by_role("status")).to_contain_text("השינויים נשמרו")
154
  assert writes[-1] == {"name": row["name"], "emails": row["emails"],
155
- "teachers": row["teachers"], "lessons": ["f", "g"]}
156
  page.locator('.mx-x').click()
157
- playwright.expect(page.locator('[data-id="g_triangle_area"]')).to_be_visible()
158
  page.reload()
159
- playwright.expect(page.locator('[data-id="g_triangle_area"]')).to_be_visible()
160
  finally:
161
  browser.close()
 
102
  def test_class_management_geometry_and_permissions(phone_server, manager):
103
  import json
104
 
105
+ row = {"id": 7, "name": "כיתת תרגול", "lessons": ["t"],
106
  "emails": ["student@example.com"], "teachers": ["teacher@example.com"]}
107
  writes = []
108
  with playwright.sync_playwright() as p:
 
144
  playwright.expect(button).to_have_count(0)
145
  return
146
  button.click()
147
+ geometry = page.locator('input[name="unit"][value="a"]')
148
  geometry.check()
149
  page.get_by_role("button", name="שמירת השינויים").click()
150
  playwright.expect(page.get_by_role("status")).to_contain_text("לא הצלחנו לשמור")
 
152
  page.get_by_role("button", name="שמירת השינויים").click()
153
  playwright.expect(page.get_by_role("status")).to_contain_text("השינויים נשמרו")
154
  assert writes[-1] == {"name": row["name"], "emails": row["emails"],
155
+ "teachers": row["teachers"], "lessons": ["t", "a"]}
156
  page.locator('.mx-x').click()
157
+ playwright.expect(page.locator('[data-id="a_30_side"]')).to_be_visible()
158
  page.reload()
159
+ playwright.expect(page.locator('[data-id="a_30_side"]')).to_be_visible()
160
  finally:
161
  browser.close()
tests/test_phone_photo.py CHANGED
@@ -13,7 +13,7 @@ class GeometrySolver:
13
  assert image == b"photo"
14
  assert media_type == "image/jpeg"
15
  return {"readable": True, "problem": "triangle", "steps": [{"hint": "h", "work": "w"}],
16
- "answer": "10 cm", "skill": "g_pythagoras", "note": ""}
17
 
18
 
19
  def photo_app(monkeypatch):
 
13
  assert image == b"photo"
14
  assert media_type == "image/jpeg"
15
  return {"readable": True, "problem": "triangle", "steps": [{"hint": "h", "work": "w"}],
16
+ "answer": "10 cm", "skill": "a_30_side", "note": ""}
17
 
18
 
19
  def photo_app(monkeypatch):