Asem75 commited on
Commit
bfbbc01
·
verified ·
1 Parent(s): b83d0c2

Upload 8 files

Browse files
astronomy_events_registry.json CHANGED
@@ -14,7 +14,14 @@
14
  "dismiss_behavior": "current_trigger_only",
15
  "completion_tracking": false,
16
  "max_popup_per_event_phase": 1,
17
- "image_fallback": "assets/astronomy/default_space.webp"
 
 
 
 
 
 
 
18
  },
19
  "source_strategy": {
20
  "primary_engine": {
@@ -67,7 +74,8 @@
67
  "peak",
68
  "end"
69
  ],
70
- "match": "any"
 
71
  },
72
  "fiqh_note": "نافذة الصلاة وحكم وقتها (ومنه علاقتها بأوقات النهي) تُقرأ من fiqh_time_registry/worship_fiqh_registry لا من هذا الملف"
73
  },
@@ -150,7 +158,8 @@
150
  "peak",
151
  "end"
152
  ],
153
- "match": "any"
 
154
  },
155
  "fiqh_note": "نافذة الصلاة وحكم وقتها تُقرأ من fiqh_time_registry/worship_fiqh_registry لا من هذا الملف"
156
  },
@@ -314,15 +323,16 @@
314
  "triggers": [
315
  {
316
  "id": "sunset_check",
317
- "when": "event_day",
318
- "timing_authority": "engine",
319
- "hijri_day_in": [
320
- 29,
321
- 30
322
- ],
323
- "at": {
324
  "event": "maghrib",
325
  "offset_minutes": 0
 
 
 
 
 
 
326
  }
327
  }
328
  ],
@@ -586,8 +596,7 @@
586
  "v1_1: توحيد last_quarter مع محرك المحاكاة (كان third_quarter)",
587
  "v1_1: category_controls: الكسوف والخسوف دائمًا فعّالان، وبقية الفئات يستطيع المستخدم إيقافها",
588
  "v1_1: إضافة crescent_visibility وmoonrise_moonset",
589
- "v1_2: تصحيح مشغّل crescent_visibility إلى مفردات المشغّلات القائمة (event_day) مع شرط هجري",
590
- "v1_2: engine_limits: أرقام البحث عن الكسوف والخسوف انتقلت من worship_event_engine.js"
591
  ],
592
  "category_controls": {
593
  "note": "الكسوف والخسوف دائمًا فعّالان لارتباطهما بعبادة. الفئات الأخرى معلومات يستطيع المستخدم إيقافها.",
@@ -606,13 +615,5 @@
606
  "planetary": {
607
  "user_can_disable": true
608
  }
609
- },
610
- "engine_limits": {
611
- "eclipse_lookback_days": 1,
612
- "eclipse_peak_lookahead_days": 1,
613
- "eclipse_max_occurrences": 8,
614
- "eclipse_cache_entries": 4,
615
- "visibility_epsilon_ms": 1000,
616
- "default_visibility_min_altitude_deg": 0
617
  }
618
  }
 
14
  "dismiss_behavior": "current_trigger_only",
15
  "completion_tracking": false,
16
  "max_popup_per_event_phase": 1,
17
+ "image_fallback": "assets/astronomy/default_space.webp",
18
+ "eclipse_scan": {
19
+ "note": "حدود بحث الكسوف والخسوف في worship_event_engine. لا قيم افتراضية داخل الكود.",
20
+ "max_occurrences": 8,
21
+ "lookback_days": 1,
22
+ "cache_limit": 4,
23
+ "epsilon_ms": 1000
24
+ }
25
  },
26
  "source_strategy": {
27
  "primary_engine": {
 
74
  "peak",
75
  "end"
76
  ],
77
+ "match": "any",
78
+ "altitude_reference": "geometric_center"
79
  },
80
  "fiqh_note": "نافذة الصلاة وحكم وقتها (ومنه علاقتها بأوقات النهي) تُقرأ من fiqh_time_registry/worship_fiqh_registry لا من هذا الملف"
81
  },
 
158
  "peak",
159
  "end"
160
  ],
161
+ "match": "any",
162
+ "altitude_reference": "geometric_center"
163
  },
164
  "fiqh_note": "نافذة الصلاة وحكم وقتها تُقرأ من fiqh_time_registry/worship_fiqh_registry لا من هذا الملف"
165
  },
 
323
  "triggers": [
324
  {
325
  "id": "sunset_check",
326
+ "when": "anchor",
327
+ "anchor": {
 
 
 
 
 
328
  "event": "maghrib",
329
  "offset_minutes": 0
330
+ },
331
+ "condition": {
332
+ "hijri_day_in": [
333
+ 29,
334
+ 30
335
+ ]
336
  }
337
  }
338
  ],
 
596
  "v1_1: توحيد last_quarter مع محرك المحاكاة (كان third_quarter)",
597
  "v1_1: category_controls: الكسوف والخسوف دائمًا فعّالان، وبقية الفئات يستطيع المستخدم إيقافها",
598
  "v1_1: إضافة crescent_visibility وmoonrise_moonset",
599
+ "v1_2: defaults.eclipse_scan (كانت أرقام داخل المحرك)، وتوحيد مرجع الارتفاع geometric_center مع Astronomy.SearchAltitude"
 
600
  ],
601
  "category_controls": {
602
  "note": "الكسوف والخسوف دائمًا فعّالان لارتباطهما بعبادة. الفئات الأخرى معلومات يستطيع المستخدم إيقافها.",
 
615
  "planetary": {
616
  "user_can_disable": true
617
  }
 
 
 
 
 
 
 
 
618
  }
619
  }
fiqh_time_registry.json CHANGED
@@ -1,725 +1,717 @@
1
  {
2
- "schema_name": "islamic_clock_fiqh_time_registry",
3
- "schema_version": "1.6.0",
4
- "registry_id": "fiqh_time_registry",
5
- "project": "الساعة الإسلامية / الخط الزمني الروحي",
6
- "language": "ar",
7
- "rtl": true,
8
- "status": "time_boundaries_registry",
9
- "purpose": "تعريف محطات وحدود الزمن التي تعتمد عليها المسطرة. لا يحتوي هذا الملف على ألوان أو أحكام عبادات أو محتوى.",
10
- "separation_contract": {
11
- "this_registry_owns": [
12
- "أسماء الحدود الزمنية",
13
- "ترتيب الحدود في الدورة اليومية",
14
- "نوع مصدر كل حد: فلكي أو محرك صلاة أو مشتق",
15
- "علاقات البداية والنهاية بين الحدود"
16
- ],
17
- "this_registry_does_not_own": [
18
- "ألوان المسطرة",
19
- "حكم النهي أو الاستحباب",
20
- "محتوى العبادات",
21
- "النوافذ المنبثقة",
22
- "تسجيل إنجاز العبادات"
23
- ]
24
- },
25
- "clock_display": {
26
- "format": "12_hour",
27
- "arabic_dayparts": true,
28
- "examples": [
29
- "6:00 صباحًا",
30
- "12:00 ظهرًا",
31
- "4:00 مساءً",
32
- "12:00 منتصف الليل"
33
- ],
34
- "forbid_24_hour_labels_in_ui": true
35
- },
36
- "daily_cycle": {
37
- "anchor": "false_dawn",
38
- "continues_into_next_day": true,
39
- "end_boundary": "next_false_dawn",
40
- "ui_model": "continuous_horizontal_ruler",
41
- "note": "الدورة الزمنية متصلة؛ الوصول إلى نهاية الدورة يدخل مباشرة في دورة اليوم التالي."
42
- },
43
- "authorities": {
44
- "astronomy_engine": {
45
- "role": "حساب موضع الشمس والحدود الفلكية والرصدية",
46
- "hardcoded_clock_minutes_for_solar_boundaries": false
47
- },
48
- "prayer_engine": {
49
- "role": "إعطاء أوقات الصلاة المعتمدة حسب إعدادات التطبيق"
50
- },
51
- "derived_time_engine": {
52
- "role": "اشتقاق الحدود الزمنية المركبة من حدود محسوبة أخرى"
53
- },
54
- "fiqh_profile": {
55
- "role": "اختيار الحد الفقهي عندما توجد أكثر من طريقة معتبرة لتفسير حد زمني",
56
- "note": "هذا الملف يعرّف الزمن فقط؛ الحكم التعبدي يوضع في Registry مستقل."
57
- }
58
  },
59
- "boundaries": [
60
- {
61
- "id": "false_dawn",
62
- "label": "الفجر الكاذب",
63
- "source": "astronomy_engine",
64
- "kind": "observational_estimate",
65
- "fixed_minutes": false,
66
- "note": "حد رصدي/فلكي تقديري مستقل عن الفجر الصادق."
67
- },
68
- {
69
- "id": "true_dawn",
70
- "label": "الفجر الصادق",
71
- "source": "prayer_engine",
72
- "kind": "prayer_boundary",
73
- "fixed_minutes": false
74
- },
75
- {
76
- "id": "sunrise",
77
- "label": "الشروق",
78
- "source": "astronomy_engine",
79
- "kind": "solar_boundary",
80
- "fixed_minutes": false
81
- },
82
- {
83
- "id": "sunrise_clear",
84
- "label": "ارتفاع الشمس بعد الشروق",
85
- "source": "astronomy_engine",
86
- "kind": "solar_boundary",
87
- "fixed_minutes": false,
88
- "note": "يحسب من حركة الشمس؛ لا يُختزل في عدد دقائق ثابت بعد الشروق."
89
- },
90
- {
91
- "id": "solar_noon",
92
- "label": "الاستواء الشمسي",
93
- "source": "astronomy_engine",
94
- "kind": "solar_boundary",
95
- "fixed_minutes": false
96
- },
97
- {
98
- "id": "zawal",
99
- "label": "الزوال",
100
- "source": "astronomy_engine",
101
- "kind": "solar_transition",
102
- "fixed_minutes": false,
103
- "relation": "بعد الاستواء عندما تبدأ الشمس بالميل عن أعلى ارتفاع",
104
- "practical_equivalence": "يتطابق عمليًا مع الاستواء ضمن دقة الحل العددي (نحو ثانية). النافذة المنهيّ فيها قبيل الزوال سياسة عرض في worship_fiqh_registry ولا تُشتق من هذا الحد."
105
- },
106
- {
107
- "id": "dhuhr",
108
- "label": "الظهر",
109
- "source": "prayer_engine",
110
- "kind": "prayer_boundary",
111
- "fixed_minutes": false
112
- },
113
- {
114
- "id": "asr_shadow_1",
115
- "label": "حد الظل: المثل",
116
- "source": "astronomy_engine",
117
- "kind": "shadow_factor_boundary",
118
- "shadow_factor": 1,
119
- "exclude_noon_shadow": true,
120
- "fixed_minutes": false,
121
- "note": "يحسب من طول الظل بعد استبعاد فيء الزوال؛ يستخدمه ملف الفقه لبدء العصر عند الجمهور وبعض الأقوال الحنفية."
122
- },
123
- {
124
- "id": "asr_shadow_2",
125
- "label": "حد الظل: المثلان",
126
- "source": "astronomy_engine",
127
- "kind": "shadow_factor_boundary",
128
- "shadow_factor": 2,
129
- "exclude_noon_shadow": true,
130
- "fixed_minutes": false,
131
- "note": "يحسب من طول الظل بعد استبعاد فيء الزوال؛ يستخدمه ملف الفقه لقول أبي حنيفة المشهور في بدء العصر."
132
- },
133
- {
134
- "id": "asr",
135
- "label": "العصر",
136
- "source": "prayer_engine",
137
- "kind": "prayer_boundary",
138
- "fixed_minutes": false
139
- },
140
- {
141
- "id": "sunset_approach",
142
- "label": "اقتراب الغروب",
143
- "source": "astronomy_engine",
144
- "kind": "solar_boundary",
145
- "fixed_minutes": false,
146
- "note": "لا يساوي وقت العصر ولا يُحدد برقم ساعة ثابت."
147
- },
148
- {
149
- "id": "sunset",
150
- "label": "الغروب",
151
- "source": "astronomy_engine",
152
- "kind": "solar_boundary",
153
- "fixed_minutes": false
154
- },
155
- {
156
- "id": "maghrib",
157
- "label": "المغرب",
158
- "source": "prayer_engine",
159
- "kind": "prayer_boundary",
160
- "fixed_minutes": false
161
- },
162
- {
163
- "id": "red_twilight_end",
164
- "label": "نهاية الشفق الأحمر",
165
- "source": "astronomy_engine",
166
- "kind": "observational_estimate",
167
- "fixed_minutes": false,
168
- "note": "حد رصدي تقديري مستقل عن حد دخول العشاء الفقهي."
169
- },
170
- {
171
- "id": "white_twilight_end",
172
- "label": "نهاية الشفق الأبيض",
173
- "source": "astronomy_engine",
174
- "kind": "observational_estimate",
175
- "fixed_minutes": false,
176
- "note": "حد رصدي تقديري مستقل عن حد دخول العشاء الفقهي."
177
- },
178
- {
179
- "id": "isha",
180
- "label": "العشاء",
181
- "source": "prayer_engine",
182
- "kind": "prayer_boundary",
183
- "fixed_minutes": false
184
- },
185
- {
186
- "id": "first_third_of_night",
187
- "label": "نهاية الثلث الأول من الليل",
188
- "source": "derived_time_engine",
189
- "kind": "derived_boundary",
190
- "formula": "sunset + (1/3 * duration(sunset, next_true_dawn))",
191
- "fixed_minutes": false,
192
- "note": "حد حسابي فقط تستخدمه طبقة الفقه عند تمييز فترات العشاء."
193
- },
194
- {
195
- "id": "islamic_midnight",
196
- "label": "منتصف الليل الشرعي",
197
- "source": "derived_time_engine",
198
- "kind": "derived_boundary",
199
- "formula": "midpoint(sunset, next_true_dawn)",
200
- "fixed_minutes": false
201
- },
202
- {
203
- "id": "last_third_start",
204
- "label": "بداية الثلث الأخير من الليل",
205
- "source": "derived_time_engine",
206
- "kind": "derived_boundary",
207
- "formula": "sunset + (2/3 * duration(sunset, next_true_dawn))",
208
- "fixed_minutes": false
209
- },
210
- {
211
- "id": "next_false_dawn",
212
- "label": "الفجر الكاذب لليوم التالي",
213
- "source": "astronomy_engine",
214
- "kind": "observational_estimate",
215
- "fixed_minutes": false,
216
- "cycle_link": "false_dawn"
217
- },
218
- {
219
- "id": "next_true_dawn",
220
- "label": "الفجر الصادق لليوم التالي",
221
- "source": "prayer_engine",
222
- "kind": "prayer_boundary",
223
- "fixed_minutes": false,
224
- "cycle_link": "true_dawn"
225
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  ],
227
- "derived_windows": [
228
- {
229
- "id": "post_sunrise_to_sunrise_clear",
230
- "start": "sunrise",
231
- "end": "sunrise_clear",
232
- "semantic": "فترة زمنية فقط؛ الحكم واللون يحددان في ملفات مستقلة"
233
- },
234
- {
235
- "id": "duha_time_geometry",
236
- "start": "sunrise_clear",
237
- "end": "solar_noon",
238
- "semantic": "نافذة زمنية هندسية فقط؛ لا تحمل حكمًا أو لونًا هنا"
239
- },
240
- {
241
- "id": "solar_noon_to_zawal",
242
- "start": "solar_noon",
243
- "end": "zawal",
244
- "semantic": "انتقال الاستواء إلى الزوال؛ الحكم واللون خارج هذا الملف"
245
- },
246
- {
247
- "id": "sunset_approach_to_sunset",
248
- "start": "sunset_approach",
249
- "end": "sunset",
250
- "semantic": "فترة اقترا�� الغروب فقط؛ لا تحمل حكمًا فقهيًا هنا"
251
- },
252
- {
253
- "id": "night",
254
- "start": "sunset",
255
- "end": "next_true_dawn",
256
- "semantic": "الفترة الليلية المستخدمة للاشتقاقات الزمنية"
257
- },
258
- {
259
- "id": "first_third_of_night_window",
260
- "start": "sunset",
261
- "end": "first_third_of_night",
262
- "semantic": "نافذة حسابية فقط؛ الحكم الفقهي خارج هذا الملف."
263
- },
264
- {
265
- "id": "last_third_of_night_window",
266
- "start": "last_third_start",
267
- "end": "next_true_dawn",
268
- "semantic": "نافذة حسابية فقط؛ الحكم والعبادة خارج هذا الملف."
269
- }
270
  ],
271
- "ordering": [
272
- "false_dawn",
273
- "true_dawn",
274
- "sunrise",
275
- "sunrise_clear",
276
- "solar_noon",
277
- "zawal",
278
- "dhuhr",
279
- "asr_shadow_1",
280
- "asr_shadow_2",
281
- "asr",
282
- "sunset_approach",
283
- "sunset",
284
- "maghrib",
285
- "red_twilight_end",
286
- "white_twilight_end",
287
- "isha",
288
- "first_third_of_night",
289
- "islamic_midnight",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  "last_third_start",
291
- "next_false_dawn",
292
  "next_true_dawn"
293
- ],
294
- "validation": {
295
- "no_colors": true,
296
- "no_worship_rulings": true,
297
- "no_fixed_solar_minute_offsets": true,
298
- "continuous_cycle": true,
299
- "implementation_ready": true,
300
- "supports_madhhab_worship_registry": true,
301
- "shadow_factor_boundaries_present": true,
302
- "night_fraction_boundaries_present": true,
303
- "false_dawn_boundary_defined": true,
304
- "solver_numbers_in_registry": true
305
- },
306
- "integration_contract": {
307
- "worship_fiqh_registry": "worship_fiqh_registry.json",
308
- "role_ar": "هذا الملف يحسب الحدود فقط، وملف worship_fiqh_registry.json يختار أي حد تستخدمه العبادة بحسب المذهب.",
309
- "madhhab_sensitive_outputs": [
310
- "asr_shadow_1",
311
- "asr_shadow_2",
312
- "first_third_of_night",
313
- "red_twilight_end",
314
- "white_twilight_end"
315
- ]
316
- },
317
- "calculation_profile": {
318
- "id": "inherited_solar_estimates_v1",
319
- "label_ar": "إعدادات شمسية تقديرية من النسخة القديمة",
320
- "note_ar": "حساب شمسي تقديري يتغير بالموقع والتاريخ. ارتفاع الشمس 4.5° والشفق 15°/18° إعدادات تقديرية موروثة تحتاج اعتمادًا. الاستواء والزوال حدّا انتقال اتجاه حركة الشمس، دون افتراض فترة بالدقائق؛ هذه ليست مواقيت أذان رسمية.",
321
- "provenance": {
322
- "solar_geometry": "index-8.html: sunPosition / solarAltitudeAt",
323
- "sunrise_clear": "index-8.html: solarFiqhBoundaries, 4.5 degrees",
324
- "zawal": "fiqh_time_registry_solar_v1_1.json: post_transit_transition / after_upper_solar_transit؛ حل عددي لتغير اتجاه الارتفاع بعد القمة، دون مساواة alias أو عتبة انخفاض مخترعة.",
325
- "sunset_approach": "index-8.html: estimateSunsetStart, -0.30 degrees",
326
- "twilight": "spiritual_timeline_v3_fiqh_timers_FINAL.zip: fiqh_time_registry.json, red 15 and white 18 degrees",
327
- "true_dawn": "index-8.html: FAJR_ANGLE = 18"
328
- },
329
- "default_madhhab": "shafii",
330
- "locations": [
331
- {
332
- "name": "عمّان",
333
- "lat": 31.9454,
334
- "lng": 35.9284,
335
- "timezone": "Asia/Amman",
336
- "height": 0
337
- }
338
- ],
339
- "observational_limits": {
340
- "false_dawn": null,
341
- "next_false_dawn": null
342
- },
343
- "boundaries": {
344
- "false_dawn": {
345
- "op": "altitude",
346
- "degrees": -20,
347
- "direction": "ascending",
348
- "approximate": true,
349
- "provisional": true,
350
- "note_ar": "قيمة مؤقتة لتفعيل مرساة الدورة اليومية فقط، وهي رصدية تقديرية. تحتاج اعتماد المرجع الفلكي قبل الاعتماد النهائي، ولا تدخل في حساب أي عبادة."
351
- },
352
- "solar_noon": {
353
- "op": "maximum"
354
- },
355
- "true_dawn": {
356
- "op": "altitude",
357
- "degrees": -18,
358
- "direction": "ascending"
359
- },
360
- "sunrise": {
361
- "op": "altitude",
362
- "degrees": -0.833,
363
- "direction": "ascending"
364
- },
365
- "sunrise_clear": {
366
- "op": "altitude",
367
- "degrees": 4.5,
368
- "direction": "ascending",
369
- "approximate": true
370
- },
371
- "zawal": {
372
- "op": "post_transit_transition",
373
- "boundary": "solar_noon",
374
- "direction": "descending",
375
- "transition_only": true,
376
- "note_ar": "الزوال محسوب من بدء انخفاض ارتفاع الشمس بعد الاستواء. يعرض كحد شمسي؛ لا يستنتج الحساب منه فترة نهي ممتدة بالدقائق."
377
- },
378
- "sunset_approach": {
379
- "op": "altitude",
380
- "degrees": -0.3,
381
- "direction": "descending",
382
- "approximate": true
383
- },
384
- "sunset": {
385
- "op": "altitude",
386
- "degrees": -0.833,
387
- "direction": "descending"
388
- },
389
- "red_twilight_end": {
390
- "op": "altitude",
391
- "degrees": -15,
392
- "direction": "descending",
393
- "approximate": true
394
- },
395
- "white_twilight_end": {
396
- "op": "altitude",
397
- "degrees": -18,
398
- "direction": "descending",
399
- "approximate": true
400
- },
401
- "asr_shadow_1": {
402
- "op": "shadow",
403
- "factor": 1
404
- },
405
- "asr_shadow_2": {
406
- "op": "shadow",
407
- "factor": 2
408
- },
409
- "fajr": {
410
- "op": "alias",
411
- "boundary": "true_dawn"
412
- },
413
- "dhuhr": {
414
- "op": "alias",
415
- "boundary": "zawal"
416
- },
417
- "maghrib": {
418
- "op": "alias",
419
- "boundary": "sunset"
420
- },
421
- "asr": {
422
- "op": "rule_start",
423
- "rule_id": "asr"
424
- },
425
- "isha": {
426
- "op": "rule_start",
427
- "rule_id": "isha_entry"
428
- },
429
- "isha_entry_hanafi": {
430
- "op": "alias",
431
- "boundary": "white_twilight_end"
432
- },
433
- "next_true_dawn": {
434
- "op": "next_day",
435
- "boundary": "true_dawn"
436
- },
437
- "first_third_of_night": {
438
- "op": "fraction",
439
- "from": "sunset",
440
- "to": "next_true_dawn",
441
- "fraction": 0.3333333333333333
442
- },
443
- "islamic_midnight": {
444
- "op": "fraction",
445
- "from": "sunset",
446
- "to": "next_true_dawn",
447
- "fraction": 0.5
448
- },
449
- "last_third_start": {
450
- "op": "fraction",
451
- "from": "sunset",
452
- "to": "next_true_dawn",
453
- "fraction": 0.6666666666666666
454
- },
455
- "next_false_dawn": {
456
- "op": "next_day",
457
- "boundary": "false_dawn"
458
- }
459
  },
460
- "relation_bindings": {
461
- "after_isha": "isha",
462
- "last_third_start_to_next_true_dawn": [
463
- "last_third_start",
464
- "next_true_dawn"
465
- ]
466
  },
467
- "rule_time_bindings": {
468
- "isha_entry": {
469
- "end": "next_true_dawn"
470
- },
471
- "isha_periods": {
472
- "start": "isha"
473
- }
474
  },
475
- "notice_runtime": {
476
- "due_window_seconds": 90,
477
- "trigger_bindings": {
478
- "day_start": {
479
- "boundary": "true_dawn"
480
- },
481
- "after_fajr": {
482
- "boundary": "fajr"
483
- },
484
- "after_dhuhr": {
485
- "boundary": "dhuhr"
486
- },
487
- "after_asr": {
488
- "boundary": "asr"
489
- },
490
- "after_maghrib": {
491
- "boundary": "maghrib"
492
- },
493
- "after_isha": {
494
- "boundary": "isha"
495
- },
496
- "before_fajr": {
497
- "boundary": "fajr",
498
- "offset_sign": -1
499
- },
500
- "before_jumuah": {
501
- "boundary": "dhuhr",
502
- "offset_sign": -1
503
- },
504
- "duha_start": {
505
- "boundary": "sunrise_clear"
506
- },
507
- "last_third_start": {
508
- "boundary": "last_third_start"
509
- },
510
- "previous_evening": {
511
- "boundary": "sunset",
512
- "day_shift": -1
513
- },
514
- "sunday_evening": {
515
- "boundary": "sunset",
516
- "day_shift": -1
517
- },
518
- "wednesday_evening": {
519
- "boundary": "sunset",
520
- "day_shift": -1
521
- }
522
- },
523
- "note_ar": "نافذة استدراك التنبيه سياسة تطبيق فقط. حدود أوقات النهي لا تستخدم هذه الإزاحات. التنبيهات تعمل أثناء فتح الصفحة، ويحدّد سجل الحدث عدد مرات ظهوره."
524
  },
525
- "solar_solver": {
526
- "time_tolerance_ms": 1000,
527
- "derivative_sample_ms": 1000,
528
- "note_ar": "دقة عددية للبحث عن انقلاب اتجاه الارتفاع؛ ليست مدة نهي أو إزاحة زمنية للزوال. جميع القيم العددية للمحرك تأتي من هنا ولا شيء منها مكتوب في solar_engine.js.",
529
- "scan_step_ms": 300000,
530
- "bisect_iterations": 36,
531
- "maximum_iterations": 60,
532
- "transition_iterations": 80,
533
- "noon_search_half_window_ms": 21600000,
534
- "crossing_window_ms": 43200000,
535
- "alias_passes": 5,
536
- "use_astronomy_search_functions": true
537
  },
538
- "solar_motion": {
539
- "engine": "local_solar_geometry",
540
- "algorithm_reference": "Jean Meeus, Astronomical Algorithms",
541
- "indicator_position_source": "device_clock_on_continuous_timeline",
542
- "day_night_source": "computed_sunrise_and_sunset",
543
- "apparent_horizon_degrees": -0.833,
544
- "uses_prayer_time_provider": false,
545
- "note_ar": "حركة المؤشر تتبع وقت الجهاز على الخط المتصل، وحالة الشمس/القمر تتبع ارتفاع الشمس المحسوب للموقع. حد الشروق والغروب الظاهري −0.833°؛ وقد يختلف الرصد الفعلي قليلًا بسبب الغلاف الجوي والأفق المحلي."
546
- }
547
- },
548
- "prayer_calculation": {
549
- "default_method": "JordanAwqaf",
550
- "storage_key": "clock_calculation_method_v1",
551
- "legacy_option": {
552
- "id": "registry",
553
- "label": "الحدود الشمسية والسجل الفقهي الحالي"
554
  },
555
- "engine": {
556
- "url": "https://unpkg.com/adhan@4.4.6/lib/bundles/adhan.umd.min.js",
557
- "version": "4.4.6",
558
- "timeout_ms": 12000,
559
- "source": "Adhan JS 4.4.6؛ الحسابات الفلكية مبنية على Astronomical Algorithms لِـ Jean Meeus",
560
- "fallback_url": "vendor/adhan.umd.min.js",
561
- "offline_note_ar": "يُحمَّل من الرابط أولًا ثم من النسخة المحلية عند فشله؛ ضع الملف بجوار index.html وأضفه إلى ملفات sw.js المخزنة مؤقتًا ليعمل التطبيق دون إنترنت."
562
  },
563
- "methods": [
564
- {
565
- "id": "JordanAwqaf",
566
- "label": "الأردن — وزارة الأوقاف",
567
- "engine_mode": "custom_angles",
568
- "fajr_angle": 18,
569
- "isha_angle": 18,
570
- "adjustments": {
571
- "maghrib": 5
572
- },
573
- "provider_method_id": 23,
574
- "solar_sunset_is_independent": true,
575
- "authority_name": "وزارة الأوقاف والشؤون والمقدسات الإسلامية — الأردن",
576
- "method_reference_url": "https://api.aladhan.com/v1/methods",
577
- "daily_reference_url": "https://www.aliftaa.jo/Default",
578
- "note_ar": "تطبيق محلي لمعاملات طريقة الأردن المنشورة: الفجر 18°، العشاء 18°، والمغرب بعد الغروب بخمس دقائق. تبقى مواقيت دائرة الإفتاء اليومية مرجع المقارنة الرسمي لمدينة عمّان."
579
- },
580
- {
581
- "id": "MuslimWorldLeague",
582
- "label": "رابطة العالم الإسلامي"
583
- },
584
- {
585
- "id": "UmmAlQura",
586
- "label": "أم القرى - مكة"
587
- },
588
- {
589
- "id": "Egyptian",
590
- "label": "الهيئة المصرية للمساحة"
591
- },
592
- {
593
- "id": "Karachi",
594
- "label": "كراتشي"
595
- },
596
- {
597
- "id": "Dubai",
598
- "label": "دبي"
599
- },
600
- {
601
- "id": "Qatar",
602
- "label": "قطر"
603
- },
604
- {
605
- "id": "Kuwait",
606
- "label": "الكويت"
607
- },
608
- {
609
- "id": "MoonsightingCommittee",
610
- "label": "لجنة تحري الهلال"
611
- },
612
- {
613
- "id": "Singapore",
614
- "label": "سنغافورة / ماليزيا / إندونيسيا"
615
- },
616
- {
617
- "id": "Turkey",
618
- "label": "تركيا"
619
- },
620
- {
621
- "id": "Tehran",
622
- "label": "طهران"
623
- },
624
- {
625
- "id": "NorthAmerica",
626
- "label": "ISNA - أمريكا الشمالية"
627
- }
628
- ],
629
- "shadow_factor_to_madhab": {
630
- "1": "Shafi",
631
- "2": "Hanafi"
632
  },
633
- "madhhab_engine_bindings": {
634
- "hanafi": {
635
- "asr_shadow_factor": 2,
636
- "adhan_madhab": "Hanafi",
637
- "moonsighting_shafaq": "Abyad"
638
- },
639
- "maliki": {
640
- "asr_shadow_factor": 1,
641
- "adhan_madhab": "Shafi",
642
- "moonsighting_shafaq": "Ahmer"
643
- },
644
- "shafii": {
645
- "asr_shadow_factor": 1,
646
- "adhan_madhab": "Shafi",
647
- "moonsighting_shafaq": "Ahmer"
648
- },
649
- "hanbali": {
650
- "asr_shadow_factor": 1,
651
- "adhan_madhab": "Shafi",
652
- "moonsighting_shafaq": "Ahmer"
653
- }
654
  },
655
- "output_bindings": {
656
- "fajr": "fajr",
657
- "dhuhr": "dhuhr",
658
- "asr": "asr",
659
- "maghrib": "maghrib",
660
- "isha": "isha"
661
  },
662
- "period_bindings": {
663
- "fajr": {
664
- "start": "fajr",
665
- "end": "sunrise"
666
- },
667
- "dhuhr": {
668
- "start": "dhuhr",
669
- "end": "asr"
670
- },
671
- "asr": {
672
- "start": "asr",
673
- "end": "maghrib"
674
- },
675
- "maghrib": {
676
- "start": "maghrib",
677
- "end": "isha"
678
- },
679
- "isha_entry": {
680
- "start": "isha",
681
- "end": "next_true_dawn"
682
- }
683
  },
684
- "ramadan_interval": {
685
- "method": "UmmAlQura",
686
- "minutes": 120,
687
- "storage_key": "clock_ummalqura_ramadan_v1"
688
  },
689
- "note_ar": "طريقة الأردن هي الافتراضية، وتستخدم معاملات وزارة الأوقاف المنشورة مع إبقاء صفحة دائرة الإفتاء مرجع المقارنة اليومي. فرق خمس دقائق يخص موعد المغرب في هذه الطريقة ولا يغيّر حد الغروب الشمسي المحسوب. تشمل القائمة جميع الطرق المسماة التي يوفرها Adhan JS 4.4.6 إضافة إلى طريقة الأردن المخصصة. مواقيت الصلاة لا تستبدل الحدود الشمسية المستقلة التي تحرك مؤشر الشمس. المذهب الحنفي يأخذ المثلين للعصر، والمذاهب الثلاثة تأخذ المثل؛ وتضبط طريقة لجنة تحري الهلال نوع الشفق كذلك. خيار رمضان لأم القرى يؤكد يدويًا ولا يستنتج من تقويم تقريبي.",
690
- "high_latitude": {
691
- "default": "recommended",
692
- "storage_key": "clock_high_latitude_rule_v1",
693
- "options": [
694
- {
695
- "id": "recommended",
696
- "label_ar": "الموصى به حسب خط العرض",
697
- "adhan": "HighLatitudeRule.recommended"
698
- },
699
- {
700
- "id": "MiddleOfTheNight",
701
- "label_ar": "منتصف الليل",
702
- "adhan": "HighLatitudeRule.MiddleOfTheNight"
703
- },
704
- {
705
- "id": "SeventhOfTheNight",
706
- "label_ar": "سُبع الليل",
707
- "adhan": "HighLatitudeRule.SeventhOfTheNight"
708
- },
709
- {
710
- "id": "TwilightAngle",
711
- "label_ar": "زاوية الشفق",
712
- "adhan": "HighLatitudeRule.TwilightAngle"
713
- }
714
- ],
715
- "note_ar": "في المناطق التي لا يغيب فيها الشفق أو لا تغرب فيها الشمس يخفق حساب الزاوية، فتُستخدم هذه القاعدة. يتحقق المطوّر من وجود recommended في إصدار Adhan المثبت. الحدود الشمسية المستقلة تعرض سبب التعذر في unavailable بدل قيمة وهمية."
716
  }
 
 
717
  },
718
- "changelog": [
719
- "1.6.0: تعريف حد الفجر الكاذب والتالي له (كانا مرساة الدورة ولا حساب لهما) بقيمة مؤقتة تحتاج اعتماد المرجع",
720
- "1.6.0: نقل كل أرقام الحل العددي من solar_engine.js إلى solar_solver",
721
- "1.6.0: سياسة خطوط العرض العليا (high_latitude) لمحرك الصلاة",
722
- "1.6.0: مسار محلي احتياطي لمكتبة Adhan، وخط زمني ومنطقة زمنية للموقع",
723
- "1.6.0: توضيح تطابق الزوال مع الاستواء عمليًا"
724
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  }
 
1
  {
2
+ "schema_name": "islamic_clock_fiqh_time_registry",
3
+ "schema_version": "1.6.0",
4
+ "registry_id": "fiqh_time_registry",
5
+ "project": "الساعة الإسلامية / الخط الزمني الروحي",
6
+ "language": "ar",
7
+ "rtl": true,
8
+ "status": "time_boundaries_registry",
9
+ "purpose": "تعريف محطات وحدود الزمن التي تعتمد عليها المسطرة. لا يحتوي هذا الملف على ألوان أو أحكام عبادات أو محتوى.",
10
+ "separation_contract": {
11
+ "this_registry_owns": [
12
+ "أسماء الحدود الزمنية",
13
+ "ترتيب الحدود في الدورة اليومية",
14
+ "نوع مصدر كل حد: فلكي أو محرك صلاة أو مشتق",
15
+ "علاقات البداية والنهاية بين الحدود"
16
+ ],
17
+ "this_registry_does_not_own": [
18
+ "ألوان المسطرة",
19
+ "حكم النهي أو الاستحباب",
20
+ "محتوى العبادات",
21
+ "النوافذ المنبثقة",
22
+ "تسجيل إنجاز العبادات"
23
+ ]
24
+ },
25
+ "clock_display": {
26
+ "format": "12_hour",
27
+ "arabic_dayparts": true,
28
+ "examples": [
29
+ "6:00 صباحًا",
30
+ "12:00 ظهرًا",
31
+ "4:00 مساءً",
32
+ "12:00 منتصف الليل"
33
+ ],
34
+ "forbid_24_hour_labels_in_ui": true
35
+ },
36
+ "daily_cycle": {
37
+ "anchor": "false_dawn",
38
+ "continues_into_next_day": true,
39
+ "end_boundary": "next_false_dawn",
40
+ "ui_model": "continuous_horizontal_ruler",
41
+ "note": "الدورة الزمنية متصلة؛ الوصول إلى نهاية الدورة يدخل مباشرة في دورة اليوم التالي."
42
+ },
43
+ "authorities": {
44
+ "astronomy_engine": {
45
+ "role": "حساب موضع الشمس والحدود الفلكية والرصدية",
46
+ "hardcoded_clock_minutes_for_solar_boundaries": false
 
 
 
 
 
 
 
 
 
 
 
47
  },
48
+ "prayer_engine": {
49
+ "role": "إعطاء أوقات الصلاة المعتمدة حسب إعدادات التطبيق"
50
+ },
51
+ "derived_time_engine": {
52
+ "role": "اشتقاق الحدود الزمنية المركبة من حدود محسوبة أخرى"
53
+ },
54
+ "fiqh_profile": {
55
+ "role": "اختيار الحد الفقهي عندما توجد أكثر من طريقة معتبرة لتفسير حد زمني",
56
+ "note": "هذا الملف يعرّف الزمن فقط؛ الحكم التعبدي يوضع في Registry مستقل."
57
+ }
58
+ },
59
+ "boundaries": [
60
+ {
61
+ "id": "false_dawn",
62
+ "label": "الفجر الكاذب",
63
+ "source": "astronomy_engine",
64
+ "kind": "observational_estimate",
65
+ "fixed_minutes": false,
66
+ "note": "حد رصدي/فلكي تقديري مستقل عن الفجر الصادق."
67
+ },
68
+ {
69
+ "id": "true_dawn",
70
+ "label": "الفجر الصادق",
71
+ "source": "prayer_engine",
72
+ "kind": "prayer_boundary",
73
+ "fixed_minutes": false
74
+ },
75
+ {
76
+ "id": "sunrise",
77
+ "label": "الشروق",
78
+ "source": "astronomy_engine",
79
+ "kind": "solar_boundary",
80
+ "fixed_minutes": false
81
+ },
82
+ {
83
+ "id": "sunrise_clear",
84
+ "label": "ارتفاع الشمس بعد الشروق",
85
+ "source": "astronomy_engine",
86
+ "kind": "solar_boundary",
87
+ "fixed_minutes": false,
88
+ "note": "يحسب من حركة الشمس؛ لا يُختزل في عدد دقائق ثابت بعد الشروق."
89
+ },
90
+ {
91
+ "id": "solar_noon",
92
+ "label": "الاستواء الشمسي",
93
+ "source": "astronomy_engine",
94
+ "kind": "solar_boundary",
95
+ "fixed_minutes": false
96
+ },
97
+ {
98
+ "id": "zawal",
99
+ "label": "الزوال",
100
+ "source": "astronomy_engine",
101
+ "kind": "solar_transition",
102
+ "fixed_minutes": false,
103
+ "relation": "بعد الاستواء عندما تبدأ الشمس بالميل عن أعلى ارتفاع"
104
+ },
105
+ {
106
+ "id": "dhuhr",
107
+ "label": "الظهر",
108
+ "source": "prayer_engine",
109
+ "kind": "prayer_boundary",
110
+ "fixed_minutes": false
111
+ },
112
+ {
113
+ "id": "asr_shadow_1",
114
+ "label": "حد الظل: المثل",
115
+ "source": "astronomy_engine",
116
+ "kind": "shadow_factor_boundary",
117
+ "shadow_factor": 1,
118
+ "exclude_noon_shadow": true,
119
+ "fixed_minutes": false,
120
+ "note": "يحسب من طول الظل بعد استبعاد فيء الزوال؛ يستخدمه ملف الفقه لبدء العصر عند الجمهور وبعض الأقوال الحنفية."
121
+ },
122
+ {
123
+ "id": "asr_shadow_2",
124
+ "label": "حد الظل: المثلان",
125
+ "source": "astronomy_engine",
126
+ "kind": "shadow_factor_boundary",
127
+ "shadow_factor": 2,
128
+ "exclude_noon_shadow": true,
129
+ "fixed_minutes": false,
130
+ "note": "يحسب من طول الظل بعد استبعاد فيء الزوال؛ يستخدمه ملف الفقه لقول أبي حنيفة المشهور في بدء العصر."
131
+ },
132
+ {
133
+ "id": "asr",
134
+ "label": "العصر",
135
+ "source": "prayer_engine",
136
+ "kind": "prayer_boundary",
137
+ "fixed_minutes": false
138
+ },
139
+ {
140
+ "id": "last_daylight_hour_start",
141
+ "label": "بداية آخر جزء من النهار",
142
+ "source": "derived_time_engine",
143
+ "kind": "derived_boundary",
144
+ "formula": "sunrise + (11/12 * duration(sunrise, sunset))",
145
+ "fixed_minutes": false,
146
+ "note": "النهار اثنا عشر جزءًا متساويًا بين الشروق والغروب؛ الحد حسابي فقط وتستخدمه طبقة الفقه لعرض ساعة الإجابة."
147
+ },
148
+ {
149
+ "id": "sunset_approach",
150
+ "label": "اقتراب الغروب",
151
+ "source": "astronomy_engine",
152
+ "kind": "solar_boundary",
153
+ "fixed_minutes": false,
154
+ "note": "لا يساوي وقت العصر ولا يُحدد برقم ساعة ثابت."
155
+ },
156
+ {
157
+ "id": "sunset",
158
+ "label": "الغروب",
159
+ "source": "astronomy_engine",
160
+ "kind": "solar_boundary",
161
+ "fixed_minutes": false
162
+ },
163
+ {
164
+ "id": "maghrib",
165
+ "label": "المغرب",
166
+ "source": "prayer_engine",
167
+ "kind": "prayer_boundary",
168
+ "fixed_minutes": false
169
+ },
170
+ {
171
+ "id": "red_twilight_end",
172
+ "label": "نهاية الشفق الأحمر",
173
+ "source": "astronomy_engine",
174
+ "kind": "observational_estimate",
175
+ "fixed_minutes": false,
176
+ "note": "حد رصدي تقديري مستقل عن حد دخول العشاء الفقهي."
177
+ },
178
+ {
179
+ "id": "white_twilight_end",
180
+ "label": "نهاية الشفق الأبيض",
181
+ "source": "astronomy_engine",
182
+ "kind": "observational_estimate",
183
+ "fixed_minutes": false,
184
+ "note": "حد رصدي تقديري مستقل عن حد دخول العشاء الفقهي."
185
+ },
186
+ {
187
+ "id": "isha",
188
+ "label": "العشاء",
189
+ "source": "prayer_engine",
190
+ "kind": "prayer_boundary",
191
+ "fixed_minutes": false
192
+ },
193
+ {
194
+ "id": "first_third_of_night",
195
+ "label": "نهاية الثلث الأول من الليل",
196
+ "source": "derived_time_engine",
197
+ "kind": "derived_boundary",
198
+ "formula": "sunset + (1/3 * duration(sunset, next_true_dawn))",
199
+ "fixed_minutes": false,
200
+ "note": "حد حسابي فقط تستخدمه طبقة الفقه عند تمييز فترات العشاء."
201
+ },
202
+ {
203
+ "id": "islamic_midnight",
204
+ "label": "منتصف الليل الشرعي",
205
+ "source": "derived_time_engine",
206
+ "kind": "derived_boundary",
207
+ "formula": "midpoint(sunset, next_true_dawn)",
208
+ "fixed_minutes": false
209
+ },
210
+ {
211
+ "id": "last_third_start",
212
+ "label": "بداية الثلث الأخير من الليل",
213
+ "source": "derived_time_engine",
214
+ "kind": "derived_boundary",
215
+ "formula": "sunset + (2/3 * duration(sunset, next_true_dawn))",
216
+ "fixed_minutes": false
217
+ },
218
+ {
219
+ "id": "next_false_dawn",
220
+ "label": "الفجر الكاذب لليوم التالي",
221
+ "source": "astronomy_engine",
222
+ "kind": "observational_estimate",
223
+ "fixed_minutes": false,
224
+ "cycle_link": "false_dawn"
225
+ },
226
+ {
227
+ "id": "next_true_dawn",
228
+ "label": "الفجر الصادق لليوم التالي",
229
+ "source": "prayer_engine",
230
+ "kind": "prayer_boundary",
231
+ "fixed_minutes": false,
232
+ "cycle_link": "true_dawn"
233
+ }
234
+ ],
235
+ "aliases": {
236
+ "note": "أسماء بديلة تُحسب في calculation_profile.boundaries وتحيل إلى حد معرّف، وتُستعمل في worship_fiqh_registry ومشغلات التنبيه.",
237
+ "fajr": {
238
+ "alias_of": "true_dawn",
239
+ "label": "الفجر"
240
+ },
241
+ "isha_entry_hanafi": {
242
+ "alias_of": "white_twilight_end",
243
+ "label": "دخول العشاء عند الحنفية (الشفق الأبيض)"
244
+ }
245
+ },
246
+ "derived_windows": [
247
+ {
248
+ "id": "post_sunrise_to_sunrise_clear",
249
+ "start": "sunrise",
250
+ "end": "sunrise_clear",
251
+ "semantic": "فترة زمنية فقط؛ الحكم واللون يحددان في ملفات مستقلة"
252
+ },
253
+ {
254
+ "id": "duha_time_geometry",
255
+ "start": "sunrise_clear",
256
+ "end": "solar_noon",
257
+ "semantic": "نافذة زمنية هندسية فقط؛ لا تحمل حكمًا أو لونًا هنا"
258
+ },
259
+ {
260
+ "id": "solar_noon_to_zawal",
261
+ "start": "solar_noon",
262
+ "end": "zawal",
263
+ "semantic": "انتقال الاستواء إلى الزوال؛ الحكم واللون خارج هذا الملف"
264
+ },
265
+ {
266
+ "id": "sunset_approach_to_sunset",
267
+ "start": "sunset_approach",
268
+ "end": "sunset",
269
+ "semantic": "فترة اقتراب الغروب فقط؛ لا تحمل حكمًا فقهيًا هنا"
270
+ },
271
+ {
272
+ "id": "night",
273
+ "start": "sunset",
274
+ "end": "next_true_dawn",
275
+ "semantic": "الفترة الليلية المستخدمة للاشتقاقات الزمنية"
276
+ },
277
+ {
278
+ "id": "first_third_of_night_window",
279
+ "start": "sunset",
280
+ "end": "first_third_of_night",
281
+ "semantic": "نافذة حسابية فقط؛ الحكم الفقهي خارج هذا الملف."
282
+ },
283
+ {
284
+ "id": "last_third_of_night_window",
285
+ "start": "last_third_start",
286
+ "end": "next_true_dawn",
287
+ "semantic": "نافذة حسابية فقط؛ الحكم والعبادة خارج هذا الملف."
288
+ }
289
+ ],
290
+ "ordering": [
291
+ "false_dawn",
292
+ "true_dawn",
293
+ "sunrise",
294
+ "sunrise_clear",
295
+ "solar_noon",
296
+ "zawal",
297
+ "dhuhr",
298
+ "asr_shadow_1",
299
+ "asr_shadow_2",
300
+ "asr",
301
+ "last_daylight_hour_start",
302
+ "sunset_approach",
303
+ "sunset",
304
+ "maghrib",
305
+ "red_twilight_end",
306
+ "white_twilight_end",
307
+ "isha",
308
+ "first_third_of_night",
309
+ "islamic_midnight",
310
+ "last_third_start",
311
+ "next_false_dawn",
312
+ "next_true_dawn"
313
+ ],
314
+ "validation": {
315
+ "no_colors": true,
316
+ "no_worship_rulings": true,
317
+ "no_fixed_solar_minute_offsets": true,
318
+ "continuous_cycle": true,
319
+ "implementation_ready": true,
320
+ "supports_madhhab_worship_registry": true,
321
+ "shadow_factor_boundaries_present": true,
322
+ "night_fraction_boundaries_present": true,
323
+ "aliases_resolve": true,
324
+ "solver_constants_in_registry": true
325
+ },
326
+ "integration_contract": {
327
+ "worship_fiqh_registry": "worship_fiqh_registry.json",
328
+ "role_ar": "هذا الملف يحسب الحدود فقط، وملف worship_fiqh_registry.json يختار أي حد تستخدمه العبادة بحسب المذهب.",
329
+ "madhhab_sensitive_outputs": [
330
+ "asr_shadow_1",
331
+ "asr_shadow_2",
332
+ "first_third_of_night",
333
+ "red_twilight_end",
334
+ "white_twilight_end"
335
  ],
336
+ "hijri_calendar_registry": "hijri_calendar_registry.json",
337
+ "cross_registry_validator": "validate_registries.js"
338
+ },
339
+ "calculation_profile": {
340
+ "id": "inherited_solar_estimates_v1",
341
+ "label_ar": "إعدادات شمسية تقديرية من النسخة القديمة",
342
+ "note_ar": "حساب شمسي تقديري يتغير بالموقع والتاريخ. ارتفاع الشمس 4.5° والشفق 15°/18° إعدادات تقديرية موروثة تحتاج اعتمادًا. الاستواء والزوال حدّا انتقال اتجاه حركة الشمس، دون افتراض فترة بالدقائق؛ هذه ليست مواقيت أذان رسمية.",
343
+ "provenance": {
344
+ "solar_geometry": "index-8.html: sunPosition / solarAltitudeAt",
345
+ "sunrise_clear": "index-8.html: solarFiqhBoundaries, 4.5 degrees",
346
+ "zawal": "fiqh_time_registry_solar_v1_1.json: post_transit_transition / after_upper_solar_transit؛ حل عددي لتغير اتجاه الارتفاع بعد القمة، دون مساواة alias أو عتبة انخفاض مخترعة.",
347
+ "sunset_approach": "index-8.html: estimateSunsetStart, -0.30 degrees",
348
+ "twilight": "spiritual_timeline_v3_fiqh_timers_FINAL.zip: fiqh_time_registry.json, red 15 and white 18 degrees",
349
+ "true_dawn": "index-8.html: FAJR_ANGLE = 18"
350
+ },
351
+ "default_madhhab": "shafii",
352
+ "locations": [
353
+ {
354
+ "id": "amman",
355
+ "name": "عمّان",
356
+ "lat": 31.9454,
357
+ "lng": 35.9284,
358
+ "height": 0,
359
+ "timezone": "Asia/Amman",
360
+ "note": "height يستهلكه solar_engine. timezone للمستهلك: حد اليوم يحتاج منطقة المدينة لا الجهاز عند اختلافهما."
361
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  ],
363
+ "observational_limits": {
364
+ "false_dawn": null,
365
+ "next_false_dawn": null
366
+ },
367
+ "boundaries": {
368
+ "solar_noon": {
369
+ "op": "maximum"
370
+ },
371
+ "true_dawn": {
372
+ "op": "altitude",
373
+ "degrees": -18,
374
+ "direction": "ascending"
375
+ },
376
+ "sunrise": {
377
+ "op": "altitude",
378
+ "degrees": -0.833,
379
+ "direction": "ascending"
380
+ },
381
+ "sunrise_clear": {
382
+ "op": "altitude",
383
+ "degrees": 4.5,
384
+ "direction": "ascending",
385
+ "approximate": true
386
+ },
387
+ "zawal": {
388
+ "op": "post_transit_transition",
389
+ "boundary": "solar_noon",
390
+ "direction": "descending",
391
+ "transition_only": true,
392
+ "note_ar": "الزوال محسوب من بدء انخفاض ارتفاع الشمس بعد الاستواء. يعرض كحد شمسي؛ لا يستنتج الحساب منه فترة نهي ممتدة بالدقائق."
393
+ },
394
+ "sunset_approach": {
395
+ "op": "altitude",
396
+ "degrees": -0.3,
397
+ "direction": "descending",
398
+ "approximate": true
399
+ },
400
+ "sunset": {
401
+ "op": "altitude",
402
+ "degrees": -0.833,
403
+ "direction": "descending"
404
+ },
405
+ "red_twilight_end": {
406
+ "op": "altitude",
407
+ "degrees": -15,
408
+ "direction": "descending",
409
+ "approximate": true
410
+ },
411
+ "white_twilight_end": {
412
+ "op": "altitude",
413
+ "degrees": -18,
414
+ "direction": "descending",
415
+ "approximate": true
416
+ },
417
+ "asr_shadow_1": {
418
+ "op": "shadow",
419
+ "factor": 1
420
+ },
421
+ "asr_shadow_2": {
422
+ "op": "shadow",
423
+ "factor": 2
424
+ },
425
+ "fajr": {
426
+ "op": "alias",
427
+ "boundary": "true_dawn"
428
+ },
429
+ "dhuhr": {
430
+ "op": "alias",
431
+ "boundary": "zawal"
432
+ },
433
+ "maghrib": {
434
+ "op": "alias",
435
+ "boundary": "sunset"
436
+ },
437
+ "asr": {
438
+ "op": "rule_start",
439
+ "rule_id": "asr"
440
+ },
441
+ "isha": {
442
+ "op": "rule_start",
443
+ "rule_id": "isha_entry"
444
+ },
445
+ "isha_entry_hanafi": {
446
+ "op": "alias",
447
+ "boundary": "white_twilight_end"
448
+ },
449
+ "next_true_dawn": {
450
+ "op": "next_day",
451
+ "boundary": "true_dawn"
452
+ },
453
+ "first_third_of_night": {
454
+ "op": "fraction",
455
+ "from": "sunset",
456
+ "to": "next_true_dawn",
457
+ "fraction": 0.3333333333333333
458
+ },
459
+ "islamic_midnight": {
460
+ "op": "fraction",
461
+ "from": "sunset",
462
+ "to": "next_true_dawn",
463
+ "fraction": 0.5
464
+ },
465
+ "last_third_start": {
466
+ "op": "fraction",
467
+ "from": "sunset",
468
+ "to": "next_true_dawn",
469
+ "fraction": 0.6666666666666666
470
+ },
471
+ "last_daylight_hour_start": {
472
+ "op": "fraction",
473
+ "from": "sunrise",
474
+ "to": "sunset",
475
+ "fraction": 0.9166666666666666
476
+ }
477
+ },
478
+ "relation_bindings": {
479
+ "after_isha": "isha",
480
+ "last_third_start_to_next_true_dawn": [
481
  "last_third_start",
 
482
  "next_true_dawn"
483
+ ]
484
+ },
485
+ "rule_time_bindings": {
486
+ "isha_entry": {
487
+ "end": "next_true_dawn"
488
+ },
489
+ "isha_periods": {
490
+ "start": "isha"
491
+ }
492
+ },
493
+ "notice_runtime": {
494
+ "due_window_seconds": 90,
495
+ "trigger_bindings": {
496
+ "day_start": {
497
+ "boundary": "true_dawn"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  },
499
+ "after_fajr": {
500
+ "boundary": "fajr"
 
 
 
 
501
  },
502
+ "after_dhuhr": {
503
+ "boundary": "dhuhr"
 
 
 
 
 
504
  },
505
+ "after_asr": {
506
+ "boundary": "asr"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  },
508
+ "after_maghrib": {
509
+ "boundary": "maghrib"
 
 
 
 
 
 
 
 
 
 
510
  },
511
+ "after_isha": {
512
+ "boundary": "isha"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  },
514
+ "before_fajr": {
515
+ "boundary": "fajr",
516
+ "offset_sign": -1
 
 
 
 
517
  },
518
+ "before_jumuah": {
519
+ "boundary": "dhuhr",
520
+ "offset_sign": -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  },
522
+ "duha_start": {
523
+ "boundary": "sunrise_clear"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  },
525
+ "last_third_start": {
526
+ "boundary": "last_third_start"
 
 
 
 
527
  },
528
+ "previous_evening": {
529
+ "boundary": "sunset",
530
+ "day_shift": -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  },
532
+ "sunday_evening": {
533
+ "boundary": "sunset",
534
+ "day_shift": -1
 
535
  },
536
+ "wednesday_evening": {
537
+ "boundary": "sunset",
538
+ "day_shift": -1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  }
540
+ },
541
+ "note_ar": "نافذة استدراك التنبيه سياسة تطبيق فقط. حدود أوقات النهي لا تستخدم هذه الإزاحات. التنبيهات تعمل أثناء فتح الصفحة، ويحدّد سجل الحدث عدد مرات ظهوره."
542
  },
543
+ "solar_solver": {
544
+ "time_tolerance_ms": 1000,
545
+ "derivative_sample_ms": 1000,
546
+ "note_ar": "دقة عددية وحدود بحث فقط؛ ليست مدة نهي أو إزاحة زمنية للزوال. scan_step_ms خطوة مسح العبور، وbisect_iterations تكرارات التنصيف، وnoon_bracket_hours نصف نافذة البحث حول الاستواء، وhalf_day_hours نصف اليوم لنوافذ البحث، وdependency_passes أقصى عمق لاعتماد الحدود بعضها على بعض.",
547
+ "scan_step_ms": 300000,
548
+ "bisect_iterations": 36,
549
+ "maximum_iterations": 60,
550
+ "transit_iterations": 80,
551
+ "noon_bracket_hours": 6,
552
+ "half_day_hours": 12,
553
+ "dependency_passes": 5,
554
+ "longitude_degrees_per_hour": 15
555
+ },
556
+ "solar_motion": {
557
+ "engine": "local_solar_geometry",
558
+ "algorithm_reference": "Jean Meeus, Astronomical Algorithms",
559
+ "indicator_position_source": "device_clock_on_continuous_timeline",
560
+ "day_night_source": "computed_sunrise_and_sunset",
561
+ "apparent_horizon_degrees": -0.833,
562
+ "uses_prayer_time_provider": false,
563
+ "note_ar": "حركة المؤشر تتبع وقت الجهاز على الخط المتصل، وحالة الشمس/القمر تتبع ارتفاع الشمس المحسوب للموقع. حد الشروق والغروب الظاهري −0.833°؛ وقد يختلف الرصد الفعلي قليلًا بسبب الغلاف الجوي والأفق المحلي."
564
+ }
565
+ },
566
+ "prayer_calculation": {
567
+ "default_method": "JordanAwqaf",
568
+ "storage_key": "clock_calculation_method_v1",
569
+ "legacy_option": {
570
+ "id": "registry",
571
+ "label": "الحدود الشمسية والسجل الفقهي الحالي"
572
+ },
573
+ "engine": {
574
+ "url": "https://unpkg.com/adhan@4.4.6/lib/bundles/adhan.umd.min.js",
575
+ "version": "4.4.6",
576
+ "timeout_ms": 12000,
577
+ "source": "Adhan JS 4.4.6؛ الحسابات الفلكية مبنية على Astronomical Algorithms لِـ Jean Meeus",
578
+ "local_path": null,
579
+ "offline_note_ar": "يعتمد الرابط الحالي على الإنترنت. ضع نسخة adhan.umd.min.js (الإصدار 4.4.6) بجانب index.html وسمّها هنا في local_path ليعمل الحساب دون اتصال."
580
+ },
581
+ "methods": [
582
+ {
583
+ "id": "JordanAwqaf",
584
+ "label": "الأردن — وزارة الأوقاف",
585
+ "engine_mode": "custom_angles",
586
+ "fajr_angle": 18,
587
+ "isha_angle": 18,
588
+ "adjustments": {
589
+ "maghrib": 5
590
+ },
591
+ "provider_method_id": 23,
592
+ "solar_sunset_is_independent": true,
593
+ "authority_name": "وزارة ��لأوقاف والشؤون والمقدسات الإسلامية — الأردن",
594
+ "method_reference_url": "https://api.aladhan.com/v1/methods",
595
+ "daily_reference_url": "https://www.aliftaa.jo/Default",
596
+ "note_ar": "تطبيق محلي لمعاملات طريقة الأردن المنشورة: الفجر 18°، العشاء 18°، والمغرب بعد الغروب بخمس دقائق. تبقى مواقيت دائرة الإفتاء اليومية مرجع المقارنة الرسمي لمدينة عمّان."
597
+ },
598
+ {
599
+ "id": "MuslimWorldLeague",
600
+ "label": "رابطة العالم الإسلامي"
601
+ },
602
+ {
603
+ "id": "UmmAlQura",
604
+ "label": "أم القرى - مكة"
605
+ },
606
+ {
607
+ "id": "Egyptian",
608
+ "label": "الهيئة المصرية للمساحة"
609
+ },
610
+ {
611
+ "id": "Karachi",
612
+ "label": "كراتشي"
613
+ },
614
+ {
615
+ "id": "Dubai",
616
+ "label": "دبي"
617
+ },
618
+ {
619
+ "id": "Qatar",
620
+ "label": "قطر"
621
+ },
622
+ {
623
+ "id": "Kuwait",
624
+ "label": "الكويت"
625
+ },
626
+ {
627
+ "id": "MoonsightingCommittee",
628
+ "label": "لجنة تحري الهلال"
629
+ },
630
+ {
631
+ "id": "Singapore",
632
+ "label": "سنغافورة / ماليزيا / إندونيسيا"
633
+ },
634
+ {
635
+ "id": "Turkey",
636
+ "label": "تركيا"
637
+ },
638
+ {
639
+ "id": "Tehran",
640
+ "label": "طهران"
641
+ },
642
+ {
643
+ "id": "NorthAmerica",
644
+ "label": "ISNA - أمريكا الشمالية"
645
+ }
646
+ ],
647
+ "shadow_factor_to_madhab": {
648
+ "1": "Shafi",
649
+ "2": "Hanafi"
650
+ },
651
+ "madhhab_engine_bindings": {
652
+ "hanafi": {
653
+ "asr_shadow_factor": 2,
654
+ "adhan_madhab": "Hanafi",
655
+ "moonsighting_shafaq": "Abyad"
656
+ },
657
+ "maliki": {
658
+ "asr_shadow_factor": 1,
659
+ "adhan_madhab": "Shafi",
660
+ "moonsighting_shafaq": "Ahmer"
661
+ },
662
+ "shafii": {
663
+ "asr_shadow_factor": 1,
664
+ "adhan_madhab": "Shafi",
665
+ "moonsighting_shafaq": "Ahmer"
666
+ },
667
+ "hanbali": {
668
+ "asr_shadow_factor": 1,
669
+ "adhan_madhab": "Shafi",
670
+ "moonsighting_shafaq": "Ahmer"
671
+ }
672
+ },
673
+ "output_bindings": {
674
+ "fajr": "fajr",
675
+ "dhuhr": "dhuhr",
676
+ "asr": "asr",
677
+ "maghrib": "maghrib",
678
+ "isha": "isha"
679
+ },
680
+ "period_bindings": {
681
+ "fajr": {
682
+ "start": "fajr",
683
+ "end": "sunrise"
684
+ },
685
+ "dhuhr": {
686
+ "start": "dhuhr",
687
+ "end": "asr"
688
+ },
689
+ "asr": {
690
+ "start": "asr",
691
+ "end": "maghrib"
692
+ },
693
+ "maghrib": {
694
+ "start": "maghrib",
695
+ "end": "isha"
696
+ },
697
+ "isha_entry": {
698
+ "start": "isha",
699
+ "end": "next_true_dawn"
700
+ }
701
+ },
702
+ "ramadan_interval": {
703
+ "method": "UmmAlQura",
704
+ "minutes": 120,
705
+ "storage_key": "clock_ummalqura_ramadan_v1"
706
+ },
707
+ "note_ar": "طريقة الأردن هي الافتراضية، وتستخدم معاملات وزارة الأوقاف المنشورة مع إبقاء صفحة دائرة الإفتاء مرجع المقارنة اليومي. فرق خمس دقائق يخص موعد المغرب في هذه الطريقة ولا يغيّر حد الغروب الشمسي المحسوب. تشمل القائمة جميع الطرق المسماة التي يوفرها Adhan JS 4.4.6 إضافة إلى طريقة الأردن المخصصة. مواقيت الصلاة لا تستبدل الحدود الشمسية المستقلة التي تحرك مؤشر الشمس. المذهب الحنفي يأخذ المثلين للعصر، والمذاهب الثلاثة تأخذ المثل؛ وتضبط طريقة لجنة تحري الهلال نوع الشفق كذلك. خيار رمضان لأم القرى يؤكد يدويًا ولا يستنتج من تقويم تقريبي."
708
+ },
709
+ "changelog": [
710
+ "1.6.0: aliases رسمية للحدود المستخدمة في الفقه والمحرك (fajr, isha_entry_hanafi) كانت مفقودة من boundaries",
711
+ "1.6.0: كل ثوابت الحل العددي (خطوة المسح، التكرارات، نافذة البحث) انتقلت من solar_engine.js إلى solar_solver",
712
+ "1.6.0: إضافة height وtimezone لموقع عمّان (height يستهلكه المحرك، وtimezone للمستهلك المستقبلي)",
713
+ "1.6.0: حقل local_path لمحرك Adhan حتى لا يعتمد التطبيق على CDN وحده",
714
+ "1.6.0: المدقق validate_registries.js يفحص الاتساق بين هذا الملف وworship_fiqh_registry",
715
+ "1.6.0: حد last_daylight_hour_start (11/12 من النهار) لعرض ساعة الإجابة يوم الجمعة"
716
+ ]
717
  }
hijri_calendar_engine.js CHANGED
@@ -1,86 +1,76 @@
1
  'use strict';
2
 
3
  // Pure Hijri-calendar matching. UI preferences and Registry access stay in RegistryApp.
4
- // Calendar facts (months per year, month lengths) are structural; every user/app choice is a parameter.
5
  globalThis.HijriCalendarEngine = (() => {
6
- const MS_DAY=86400000;
7
- const FACTS=Object.freeze({months_per_year:12,month_lengths:Object.freeze([29,30]),max_lunar_year_days:355});
8
- const SEARCH_HORIZON_DAYS=FACTS.max_lunar_year_days*2;
9
- const WEEKDAYS=Object.freeze(['sunday','monday','tuesday','wednesday','thursday','friday','saturday']);
10
- const formatters=new Map();
11
- let defaults={calendarId:'islamic'};
12
-
13
- // RegistryApp may set the calendar from the registry once: configure({calendarId:'islamic-umalqura'}).
14
- function configure(options={}){defaults={...defaults,...options};return {...defaults};}
15
- function formatter(calendarId){
16
- let f=formatters.get(calendarId);
17
- if(!f){f=new Intl.DateTimeFormat('en-US',{calendar:calendarId,year:'numeric',month:'numeric',day:'numeric'});formatters.set(calendarId,f);}
18
- return f;
19
- }
20
- function validConfirmation(value){
21
- if(!value||!Number.isInteger(value.year)||value.year<1||!Number.isInteger(value.month)||value.month<1||value.month>FACTS.months_per_year||!FACTS.month_lengths.includes(value.length)||!/^\d{4}-\d{2}-\d{2}$/.test(value.start||''))return false;
22
- const date=new Date(value.start+'T00:00:00Z');return Number.isFinite(date.getTime())&&date.toISOString().slice(0,10)===value.start;
23
- }
24
- function parts(ms,{calendarId=defaults.calendarId,offsetDays=0,maxOffsetDays=0,confirmations=[]}={}){
25
- try{
26
- const actual=new Date(ms),civilStamp=Date.UTC(actual.getFullYear(),actual.getMonth(),actual.getDate());
27
- for(const month of confirmations){
28
- if(!validConfirmation(month))continue;
29
- const elapsed=Math.round((civilStamp-Date.parse(month.start+'T00:00:00Z'))/MS_DAY);
30
- if(elapsed>=0&&elapsed<month.length)return {year:month.year,month:month.month,day:elapsed+1,confirmed:true};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
- const date=new Date(ms),offset=Number(offsetDays),limit=Number(maxOffsetDays);
33
- if(Number.isInteger(offset)&&Number.isFinite(limit)&&Math.abs(offset)<=limit)date.setDate(date.getDate()+offset);
34
- const formatted=formatter(calendarId).formatToParts(date);
35
- const value=type=>Number(formatted.find(part=>part.type===type)?.value);
36
- const result={year:value('year'),month:value('month'),day:value('day')};
37
- return Object.values(result).every(Number.isFinite)?result:null;
38
- }catch{return null;}
39
- }
40
- // The Hijri day begins at sunset: from sunsetMs onward the date belongs to the next civil day.
41
- function partsAtSunset(ms,sunsetMs,options={}){
42
- if(!Number.isFinite(sunsetMs)||ms<sunsetMs)return parts(ms,options);
43
- const next=new Date(ms);next.setDate(next.getDate()+1);
44
- return parts(next.getTime(),options);
45
- }
46
- const civilNoon=(ms,shiftDays=0)=>{const d=new Date(ms);return new Date(d.getFullYear(),d.getMonth(),d.getDate()+shiftDays,12).getTime();};
47
- // Month containing ms: {year,month,day,length,startMs,endMs} with civil-noon stamps for the first and last day.
48
- function monthInfo(ms,options={}){
49
- const here=parts(ms,options);if(!here)return null;
50
- const startMs=civilNoon(ms,-(here.day-1));
51
- let length=here.day;
52
- for(let i=1;i<=FACTS.month_lengths[FACTS.month_lengths.length-1]+1;i++){
53
- const next=parts(civilNoon(ms,i),options);
54
- if(!next||next.month!==here.month)break;
55
- length=next.day;
56
- }
57
- return {year:here.year,month:here.month,day:here.day,length,startMs,endMs:civilNoon(startMs,length-1)};
58
- }
59
- // First civil day (noon stamp) from fromMs on whose Hijri date is {month,day}. month: number|null (any),
60
- // day: number|'last'. Returns {ms,hijri} or null (e.g. day 30 in a 29-day month is skipped).
61
- function nextOccurrence({month=null,day},fromMs,options={}){
62
- for(let i=0;i<=SEARCH_HORIZON_DAYS;i++){
63
- const stamp=civilNoon(fromMs,i),h=parts(stamp,options);
64
- if(!h||(month!=null&&h.month!==Number(month)))continue;
65
- if(day==='last'){const tomorrow=parts(civilNoon(stamp,1),options);if(tomorrow&&tomorrow.month!==h.month)return {ms:stamp,hijri:h};}
66
- else if(h.day===Number(day))return {ms:stamp,hijri:h};
67
  }
68
- return null;
69
- }
70
- function matches(event,ms,{parts:resolveParts,forBlocking=false}={}){
71
- const calendar=event?.calendar_rule||{};
72
- if(calendar.type==='daily')return true;
73
- if(calendar.type==='weekly')return calendar.weekday===WEEKDAYS[new Date(ms).getDay()];
74
- if(calendar.calendar!=='hijri')return !calendar.type;
75
- const hijri=typeof resolveParts==='function'?resolveParts(ms):null;if(!hijri)return false;
76
- if(calendar.requires_month_confirmation&&!forBlocking&&hijri.confirmed!==true)return false;
77
- if(calendar.month!=='every'&&calendar.month!=null&&Number(calendar.month)!==hijri.month)return false;
78
- if(Array.isArray(calendar.days)&&!calendar.days.map(Number).includes(hijri.day))return false;
79
- if(calendar.day!=null&&Number(calendar.day)!==hijri.day)return false;
80
- if(Array.isArray(calendar.day_range)&&(hijri.day<Number(calendar.day_range[0])||hijri.day>Number(calendar.day_range[1])))return false;
81
- if(Array.isArray(calendar.night_range)&&(hijri.day<Number(calendar.night_range[0])||hijri.day>Number(calendar.night_range[1])))return false;
82
- if(Array.isArray(calendar.odd_nights)&&!calendar.odd_nights.map(Number).includes(hijri.day))return false;
83
- return true;
84
- }
85
- return Object.freeze({version:'2.0.0',FACTS,configure,parts,partsAtSunset,monthInfo,nextOccurrence,matches,validConfirmation});
86
  })();
 
1
  'use strict';
2
 
3
  // Pure Hijri-calendar matching. UI preferences and Registry access stay in RegistryApp.
4
+ // Calendar id, month lengths and scan limits come from hijri_calendar_registry.json via configure().
5
  globalThis.HijriCalendarEngine = (() => {
6
+ // Used only when configure() was never called (older callers); the registry always wins.
7
+ const LEGACY=Object.freeze({calendar_id:'islamic',month_lengths:[29,30],months_per_year:12,max_scan_days:400});
8
+ let config=null;
9
+ const cfg=()=>config||LEGACY;
10
+ const DAY=86400000;
11
+ const api={
12
+ configure(registry){
13
+ const next={calendar_id:registry?.calendar?.calendar_id,month_lengths:registry?.month_lengths,months_per_year:registry?.months_per_year,max_scan_days:registry?.max_scan_days};
14
+ const valid=typeof next.calendar_id==='string'&&Array.isArray(next.month_lengths)&&next.month_lengths.every(Number.isInteger)&&Number.isInteger(next.months_per_year)&&next.months_per_year>0&&Number.isInteger(next.max_scan_days)&&next.max_scan_days>0;
15
+ if(!valid)throw new Error('hijri_calendar_registry is incomplete (calendar_id, month_lengths, months_per_year, max_scan_days).');
16
+ config=Object.freeze(next);return api;
17
+ },
18
+ parts(ms,{calendarId=cfg().calendar_id,offsetDays=0,maxOffsetDays=0,confirmations=[]}={}){
19
+ try{
20
+ const actual=new Date(ms),civilStamp=Date.UTC(actual.getFullYear(),actual.getMonth(),actual.getDate());
21
+ for(const month of confirmations){
22
+ if(!api.validConfirmation(month))continue;
23
+ const elapsed=Math.round((civilStamp-Date.parse(month.start+'T00:00:00Z'))/DAY);
24
+ if(elapsed>=0&&elapsed<month.length)return {year:month.year,month:month.month,day:elapsed+1,confirmed:true};
25
+ }
26
+ const date=new Date(ms),offset=Number(offsetDays),limit=Number(maxOffsetDays);
27
+ if(Number.isInteger(offset)&&Number.isFinite(limit)&&Math.abs(offset)<=limit)date.setDate(date.getDate()+offset);
28
+ const formatted=new Intl.DateTimeFormat('en-US',{calendar:calendarId,year:'numeric',month:'numeric',day:'numeric'}).formatToParts(date);
29
+ const value=type=>Number(formatted.find(part=>part.type===type)?.value);
30
+ const result={year:value('year'),month:value('month'),day:value('day')};
31
+ return Object.values(result).every(Number.isFinite)?result:null;
32
+ }catch{return null;}
33
+ },
34
+ // The Hijri day begins at sunset: from sunsetMs onward the date is the next civil day's Hijri date.
35
+ // sunsetMs comes from the solar engine; this module stays free of solar geometry.
36
+ partsAtSunset(ms,sunsetMs,options={}){
37
+ if(!Number.isFinite(sunsetMs)||ms<sunsetMs)return api.parts(ms,options);
38
+ const next=new Date(ms);next.setDate(next.getDate()+1);
39
+ return api.parts(next.getTime(),options);
40
+ },
41
+ matches(event,ms,{parts,forBlocking=false}={}){
42
+ const calendar=event?.calendar_rule||{};
43
+ if(calendar.type==='daily')return true;
44
+ if(calendar.type==='weekly'){
45
+ const weekday=new Intl.DateTimeFormat('en-US',{weekday:'long'}).format(new Date(ms)).toLowerCase();
46
+ return calendar.weekday===weekday;
47
  }
48
+ if(calendar.calendar!=='hijri')return !calendar.type;
49
+ const hijri=typeof parts==='function'?parts(ms):null;if(!hijri)return false;
50
+ if(calendar.requires_month_confirmation&&!forBlocking&&hijri.confirmed!==true)return false;
51
+ if(calendar.month!=='every'&&calendar.month!=null&&Number(calendar.month)!==hijri.month)return false;
52
+ if(Array.isArray(calendar.days)&&!calendar.days.map(Number).includes(hijri.day))return false;
53
+ if(calendar.day!=null&&Number(calendar.day)!==hijri.day)return false;
54
+ if(Array.isArray(calendar.day_range)&&(hijri.day<Number(calendar.day_range[0])||hijri.day>Number(calendar.day_range[1])))return false;
55
+ if(Array.isArray(calendar.night_range)&&(hijri.day<Number(calendar.night_range[0])||hijri.day>Number(calendar.night_range[1])))return false;
56
+ if(Array.isArray(calendar.odd_nights)&&!calendar.odd_nights.map(Number).includes(hijri.day))return false;
57
+ return true;
58
+ },
59
+ // For rules that carry a bare calendar_rule (worship_fiqh_registry): same semantics as matches().
60
+ ruleActive(calendarRule,ms,context={}){return api.matches({calendar_rule:calendarRule},ms,context);},
61
+ // First civil day (local noon) at or after fromMs on which the event matches, scanning at most max_scan_days.
62
+ nextOccurrence(event,fromMs,{parts,forBlocking=false}={}){
63
+ const start=new Date(fromMs);start.setHours(12,0,0,0);
64
+ for(let i=0;i<cfg().max_scan_days;i++){
65
+ const day=new Date(start);day.setDate(start.getDate()+i);
66
+ if(api.matches(event,day.getTime(),{parts,forBlocking}))return day.getTime();
67
+ }
68
+ return null;
69
+ },
70
+ validConfirmation(value){
71
+ if(!value||!Number.isInteger(value.year)||value.year<1||!Number.isInteger(value.month)||value.month<1||value.month>cfg().months_per_year||!cfg().month_lengths.includes(value.length)||!/^\d{4}-\d{2}-\d{2}$/.test(value.start||''))return false;
72
+ const date=new Date(value.start+'T00:00:00Z');return Number.isFinite(date.getTime())&&date.toISOString().slice(0,10)===value.start;
 
 
 
 
 
 
 
 
 
 
73
  }
74
+ };
75
+ return Object.freeze(api);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  })();
hijri_calendar_registry.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_name": "islamic_clock_hijri_calendar_registry",
3
+ "schema_version": "1.0.0",
4
+ "registry_id": "hijri_calendar",
5
+ "purpose": "إعدادات محرك التقويم الهجري. لا يحتوي أحكامًا ولا مناسبات.",
6
+ "calendar": {
7
+ "calendar_id": "islamic",
8
+ "alternatives": [
9
+ { "id": "islamic-umalqura", "label": "أم القرى" },
10
+ { "id": "islamic-civil", "label": "الجدولي المدني" },
11
+ { "id": "islamic-tbla", "label": "الجدولي الفلكي" },
12
+ { "id": "islamic-rgsa", "label": "الرؤية السعودية (تقريبي)" }
13
+ ],
14
+ "note_ar": "التقويم المحسوب تقدير. ثبوت بداية الشهر يكون بالرؤية أو إعلان الجهة المعتمدة، وتُدخَل نتيجته عبر confirmations في المحرك فتتقدم على الحساب."
15
+ },
16
+ "day_starts_at": "maghrib",
17
+ "month_lengths": [29, 30],
18
+ "months_per_year": 12,
19
+ "max_scan_days": 400,
20
+ "validation": { "no_rulings": true, "no_events": true }
21
+ }
solar_engine.js CHANGED
@@ -1,26 +1,19 @@
1
- // Astronomy Engine API adapter. UTC is used internally.
2
- // Every threshold and every numeric solver setting is read from fiqh_time_registry.json
3
- // (calculation_profile.solar_solver). Only unit conversions live in this file.
4
  globalThis.SolarEngine = (() => {
5
- const H=3600000, D=24*H, MIN=60000, TURN=360, rad=Math.PI/(TURN/2);
6
- const REQUIRED_SOLVER=['time_tolerance_ms','derivative_sample_ms','scan_step_ms','bisect_iterations','maximum_iterations',
7
- 'transition_iterations','noon_search_half_window_ms','crossing_window_ms','alias_passes'];
8
- let solverCfg=null;
 
 
 
9
  const ready=()=>Boolean(globalThis.Astronomy?.Equator&&globalThis.Astronomy?.Horizon);
10
- function readSolver(solver){
11
- if(!solver)return null;
12
- return REQUIRED_SOLVER.every(key=>Number(solver[key])>0)?solver:null;
13
- }
14
- // Call once with profile.solar_solver; day() also does this automatically.
15
- function configure(solver){solverCfg=readSolver(solver);return Boolean(solverCfg);}
16
- const noonGuess=(day,loc)=>day+D/2-loc.lng/TURN*D;
17
- const observerFor=loc=>new globalThis.Astronomy.Observer(loc.lat,loc.lng,loc.height||0);
18
-
19
  function position(ms,loc){
20
  if(!ready())return null;
21
- const A=globalThis.Astronomy,date=new Date(ms),observer=observerFor(loc);
22
  const equator=A.Equator(A.Body.Sun,date,observer,true,true);
23
- // No extra refraction here: the registry's sunrise/sunset altitude already
24
  // incorporates the standard apparent limb/refraction allowance.
25
  const horizon=A.Horizon(date,observer,equator.ra,equator.dec,null);
26
  return {decl:equator.dec,ra:equator.ra,altitude:horizon.altitude,azimuth:horizon.azimuth};
@@ -28,87 +21,68 @@ globalThis.SolarEngine = (() => {
28
  function altitude(ms,loc){
29
  return position(ms,loc)?.altitude??null;
30
  }
31
- function maximum(day,loc,cfg=solverCfg){
32
- if(!cfg)return null;
33
- const guess=noonGuess(day,loc);let lo=guess-cfg.noon_search_half_window_ms,hi=guess+cfg.noon_search_half_window_ms;
34
- for(let i=0;i<cfg.maximum_iterations;i++){const a=lo+(hi-lo)/3,b=hi-(hi-lo)/3;if(altitude(a,loc)<altitude(b,loc))lo=a;else hi=b;}
35
  return (lo+hi)/2;
36
  }
37
  // Resolve the sign change of the solar altitude rate. The bracket width is
38
  // numerical uncertainty, never a religious duration or an offset after noon.
39
- function transitTransition(day,loc,solver=solverCfg){
40
- const cfg=readSolver(solver);if(!cfg)return null;
41
- const tolerance=cfg.time_tolerance_ms,sample=cfg.derivative_sample_ms;
42
- const guess=noonGuess(day,loc);
 
 
43
  const rate=t=>altitude(t+sample/2,loc)-altitude(t-sample/2,loc);
44
- let lo=guess-cfg.noon_search_half_window_ms,hi=guess+cfg.noon_search_half_window_ms;
45
  if(!(rate(lo)>0&&rate(hi)<0))return null;
46
- for(let i=0;i<cfg.transition_iterations&&hi-lo>tolerance;i++){
47
  const mid=(lo+hi)/2;if(rate(mid)<0)hi=mid;else lo=mid;
48
  }
49
  if(!(rate(hi)<0))return null;
50
  return {at:(lo+hi)/2,after:hi,before:lo,uncertainty_ms:hi-lo};
51
  }
52
- // Fast exact search through Astronomy.SearchAltitude (geometric altitude, same
53
- // convention as position()); the sampled scan below remains the reference path.
54
- function searchAltitude(lo,hi,target,direction,loc){
55
- const A=globalThis.Astronomy;
56
- try{
57
- const hit=A.SearchAltitude(A.Body.Sun,observerFor(loc),direction==='ascending'?1:-1,new Date(lo),(hi-lo)/D,target);
58
- const at=hit?hit.date.getTime():null;
59
- return {ok:true,at:at!=null&&at>=lo&&at<=hi?at:null};
60
- }catch{return {ok:false,at:null};}
61
- }
62
- function crossing(lo,hi,target,direction,loc,cfg=solverCfg){
63
- if(!cfg)return null;
64
- if(cfg.use_astronomy_search_functions&&typeof globalThis.Astronomy?.SearchAltitude==='function'){
65
- const fast=searchAltitude(lo,hi,target,direction,loc);
66
- if(fast.ok)return fast.at;
67
- }
68
- const step=cfg.scan_step_ms,rising=direction==='ascending';let prev=lo,pv=altitude(lo,loc)-target;
69
  for(let t=lo+step;t<=hi+1;t=Math.min(t+step,hi)){
70
  const val=altitude(t,loc)-target;
71
  if(rising?pv<=0&&val>=0:pv>=0&&val<=0){
72
- let a=prev,b=t;for(let j=0;j<cfg.bisect_iterations;j++){const m=(a+b)/2,v=altitude(m,loc)-target;if(rising?v<0:v>0)a=m;else b=m;}return(a+b)/2;
73
  }
74
  if(t===hi)break;prev=t;pv=val;
75
  }
76
  return null;
77
  }
78
- // normal | always_up | always_down, from the registry's sunrise altitude. Lets the UI
79
- // explain why twilight/sunrise boundaries are unavailable instead of showing nothing.
80
- function polarState(noon,loc,profile){
81
- const horizon=profile?.boundaries?.sunrise?.degrees;
82
- if(!Number.isFinite(horizon)||!Number.isFinite(noon))return null;
83
- const top=altitude(noon,loc),bottom=altitude(noon+D/2,loc);
84
- if(top==null||bottom==null)return null;
85
- return bottom>horizon?'always_up':top<horizon?'always_down':'normal';
86
- }
87
  function day(day,loc,profile,worship,madhhab){
88
- if(!ready())return {day,boundaries:{},engineUnavailable:true,reason:'astronomy_engine_missing'};
89
- const cfg=readSolver(profile.solar_solver);
90
- if(!cfg)return {day,boundaries:{},engineUnavailable:true,reason:'solver_config_missing'};
91
- solverCfg=cfg;
92
- const transition=transitTransition(day,loc,cfg);
93
- const noon=transition?.at??maximum(day,loc,cfg), out={}, defs=profile.boundaries, win=cfg.crossing_window_ms;
 
94
  const basic=(id,def)=>{
95
  if(def.op==='maximum')return noon;
96
  if(def.op==='post_transit_transition')return transition?.after??null;
97
- if(def.op==='altitude')return crossing(def.direction==='ascending'?noon-win:noon,def.direction==='ascending'?noon:noon+win,def.degrees,def.direction,loc,cfg);
98
- if(def.op==='altitude_drop')return crossing(noon,noon+win,altitude(noon,loc)-def.degrees,'descending',loc,cfg);
99
  if(def.op==='shadow'){
100
  const peak=altitude(noon,loc);if(peak<=0)return null;
101
  const target=Math.atan(1/(def.factor+1/Math.tan(peak*rad)))/rad;
102
- return crossing(noon,noon+win,target,'descending',loc,cfg);
103
  }
104
  return null;
105
  };
106
  Object.entries(defs).forEach(([id,def])=>{out[id]=basic(id,def)});
107
- const nextNoon=maximum(day+D,loc,cfg);
108
- for(let pass=0;pass<cfg.alias_passes;pass++)Object.entries(defs).forEach(([id,def])=>{
109
  if(def.op==='alias')out[id]=out[def.boundary]??null;
110
  if(def.op==='next_day'){
111
- const target=defs[def.boundary];out[id]=target?.op==='altitude'?crossing(nextNoon-win,nextNoon,target.degrees,target.direction,loc,cfg):null;
112
  }
113
  if(def.op==='rule_start'){
114
  const rule=worship.rules.find(r=>r.id===def.rule_id),choice=rule?.madhhab?.[madhhab];
@@ -116,34 +90,34 @@ globalThis.SolarEngine = (() => {
116
  }
117
  if(def.op==='fraction')out[id]=out[def.from]!=null&&out[def.to]!=null?out[def.from]+def.fraction*(out[def.to]-out[def.from]):null;
118
  });
119
- const unavailable={};
120
- Object.entries(defs).forEach(([id,def])=>{if(out[id]==null)unavailable[id]=def.op==='altitude'||def.op==='shadow'||def.op==='next_day'||def.op==='altitude_drop'?'no_crossing':'dependency_missing';});
121
- return {day,boundaries:out,solar_transition:transition,polar:polarState(noon,loc,profile),unavailable};
122
  }
123
- // options.display_estimate_overrides: {[estimateId]:{before_minutes,after_minutes}} (user choice; honoured
124
- // only for values listed in the estimate's user_choices_before_minutes).
125
- // options.isRuleActive(rule,dayStamp): lets the calendar layer hide rules that apply only on certain dates.
126
  function periods(days,profile,worship,madhhab,styles,scheduleBindings=null,options={}){
127
- const out=[];
128
- const resolveEstimate=(id,rule)=>{
129
- if(!id)return null;
130
- const table={...(worship.display_estimates||{}),...(rule.window_display_estimates||{})};
131
- let key=id,estimate=table[key];
132
- if(estimate?.deprecated_alias_of){key=estimate.deprecated_alias_of;estimate=table[key];}
133
- if(!estimate)return null;
134
- const override=options.display_estimate_overrides?.[key];
135
- if(override&&(!estimate.user_choices_before_minutes||estimate.user_choices_before_minutes.includes(Number(override.before_minutes))))return {...estimate,...override,id:key};
136
- return {...estimate,id:key};
137
  };
 
138
  for(const d of days){const b=d.boundaries;
139
  for(const rule of worship.rules){
140
  const state=styles.category_states[rule.category];if(!state)continue;
141
- if(options.isRuleActive&&!options.isRuleActive(rule,d.day))continue;
 
 
 
 
142
  const choice=rule.madhhab?.[madhhab];const common={...rule.common,...(typeof choice==='object'?choice:{}),...profile.rule_time_bindings?.[rule.id]};
143
  if(scheduleBindings&&d.methodUnavailable&&state!==styles.category_states.solar_prayer_restriction)continue;
144
  const scheduled=scheduleBindings?.[rule.id];const windows=scheduled?[scheduled]:(typeof choice==='object'&&choice.windows)||rule.windows||[common];
145
  for(const w of windows){
146
- const weekday=['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][new Date(d.day).getUTCDay()];
147
  if((w.exclude_weekdays||common.exclude_weekdays||[]).includes(weekday))continue;
148
  const requires=w.requires||common.requires||rule.trigger?.requires;
149
  const activation=w.activation||common.activation||(requires?'after_prayer':'boundary');
@@ -151,10 +125,10 @@ globalThis.SolarEngine = (() => {
151
  // Preserve the chosen school's outer limit, not a fabricated universal end.
152
  const endId=w.end||common.end||w.outer_end||common.outer_end||w.necessity_end||common.necessity_end||rule.trigger?.end;
153
  let start=b[startId],end=b[endId];
154
- const estimate=resolveEstimate(w.display_estimate_id,rule);
155
- if(estimate){const center=b[estimate.boundary];if(center!=null){start=center-Number(estimate.before_minutes||0)*MIN;end=center+Number(estimate.after_minutes||0)*MIN;}}
156
- const endEstimate=resolveEstimate(w.display_end_estimate_id||common.display_end_estimate_id,rule);
157
- if(endEstimate){const center=b[endEstimate.boundary];if(center!=null)end=center-Number(endEstimate.before_minutes||0)*MIN;}
158
  if(start!=null&&end!=null&&end>=start){
159
  const row={id:rule.id,start,end,point:!estimate&&!endEstimate&&(end===start||Boolean(profile.boundaries[endId]?.transition_only)),startId,endId,state,rule,day:d.day,calculationMethod:scheduled?d.calculationMethod:null,conditional:rule.category==='prayer_restriction'||activation==='after_prayer'||Boolean(requires),requires,activation,displayEstimate:estimate||null,displayEndEstimate:endEstimate||null};out.push(row);
160
  for(const binding of styles.preference_bindings||[]){
@@ -170,5 +144,6 @@ globalThis.SolarEngine = (() => {
170
  }
171
  return out;
172
  }
173
- return {version:'2.0.0',ready,configure,position,altitude,maximum,transitTransition,crossing,polarState,day,periods};
 
174
  })();
 
1
+ // Astronomy Engine 2.1.19 API adapter. UTC is used internally.
2
+ // All display/ruling thresholds are read from fiqh_time_registry.json.
 
3
  globalThis.SolarEngine = (() => {
4
+ // Unit conversions only. Every tolerance, step and iteration count comes from
5
+ // profile.solar_solver in fiqh_time_registry.json (see configure()).
6
+ const H=3600000, D=24*H, rad=Math.PI/180, MIN=60000;
7
+ const WEEKDAYS=['sunday','monday','tuesday','wednesday','thursday','friday','saturday'];
8
+ let solver=null,api=null;
9
+ const need=key=>{const v=Number(solver?.[key]);if(!(v>0))throw new Error('solar_solver.'+key+' is missing in fiqh_time_registry.json (SolarEngine.configure(profile.solar_solver))');return v;};
10
+ const configure=config=>{solver=config||null;return api;};
11
  const ready=()=>Boolean(globalThis.Astronomy?.Equator&&globalThis.Astronomy?.Horizon);
 
 
 
 
 
 
 
 
 
12
  function position(ms,loc){
13
  if(!ready())return null;
14
+ const A=globalThis.Astronomy,date=new Date(ms),observer=new A.Observer(loc.lat,loc.lng,loc.height||0);
15
  const equator=A.Equator(A.Body.Sun,date,observer,true,true);
16
+ // No extra refraction here: the registry's -0.833 degree horizon already
17
  // incorporates the standard apparent limb/refraction allowance.
18
  const horizon=A.Horizon(date,observer,equator.ra,equator.dec,null);
19
  return {decl:equator.dec,ra:equator.ra,altitude:horizon.altitude,azimuth:horizon.azimuth};
 
21
  function altitude(ms,loc){
22
  return position(ms,loc)?.altitude??null;
23
  }
24
+ function maximum(day,loc){
25
+ const half=need('half_day_hours')*H,bracket=need('noon_bracket_hours')*H;
26
+ const guess=day+half-loc.lng/need('longitude_degrees_per_hour')*H;let lo=guess-bracket,hi=guess+bracket;
27
+ for(let i=0;i<need('maximum_iterations');i++){const a=lo+(hi-lo)/3,b=hi-(hi-lo)/3;if(altitude(a,loc)<altitude(b,loc))lo=a;else hi=b;}
28
  return (lo+hi)/2;
29
  }
30
  // Resolve the sign change of the solar altitude rate. The bracket width is
31
  // numerical uncertainty, never a religious duration or an offset after noon.
32
+ function transitTransition(day,loc,config){
33
+ if(config)configure(config);
34
+ const tolerance=Number(solver?.time_tolerance_ms),sample=Number(solver?.derivative_sample_ms);
35
+ if(!(tolerance>0&&sample>0))return null;
36
+ const half=need('half_day_hours')*H,bracket=need('noon_bracket_hours')*H;
37
+ const guess=day+half-loc.lng/need('longitude_degrees_per_hour')*H;
38
  const rate=t=>altitude(t+sample/2,loc)-altitude(t-sample/2,loc);
39
+ let lo=guess-bracket,hi=guess+bracket;
40
  if(!(rate(lo)>0&&rate(hi)<0))return null;
41
+ for(let i=0;i<need('transit_iterations')&&hi-lo>tolerance;i++){
42
  const mid=(lo+hi)/2;if(rate(mid)<0)hi=mid;else lo=mid;
43
  }
44
  if(!(rate(hi)<0))return null;
45
  return {at:(lo+hi)/2,after:hi,before:lo,uncertainty_ms:hi-lo};
46
  }
47
+ function crossing(lo,hi,target,direction,loc){
48
+ const step=need('scan_step_ms'),iterations=need('bisect_iterations');
49
+ const rising=direction==='ascending';let prev=lo,pv=altitude(lo,loc)-target;
50
+ // hi+1: one millisecond of numerical slack so the final bracket end is tested.
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  for(let t=lo+step;t<=hi+1;t=Math.min(t+step,hi)){
52
  const val=altitude(t,loc)-target;
53
  if(rising?pv<=0&&val>=0:pv>=0&&val<=0){
54
+ let a=prev,b=t;for(let j=0;j<iterations;j++){const m=(a+b)/2,v=altitude(m,loc)-target;if(rising?v<0:v>0)a=m;else b=m;}return(a+b)/2;
55
  }
56
  if(t===hi)break;prev=t;pv=val;
57
  }
58
  return null;
59
  }
 
 
 
 
 
 
 
 
 
60
  function day(day,loc,profile,worship,madhhab){
61
+ if(!ready())return {day,boundaries:{},engineUnavailable:true};
62
+ configure(profile.solar_solver);
63
+ if(!worship?.madhhabs?.[madhhab])throw new Error('Unknown madhhab: '+madhhab);
64
+ const half=need('half_day_hours')*H;
65
+ const transition=transitTransition(day,loc,profile.solar_solver);
66
+ const noon=transition?.at??maximum(day,loc), out={}, defs=profile.boundaries, warnings=[];
67
+ if(!transition)warnings.push('transit_unresolved');
68
  const basic=(id,def)=>{
69
  if(def.op==='maximum')return noon;
70
  if(def.op==='post_transit_transition')return transition?.after??null;
71
+ if(def.op==='altitude')return crossing(def.direction==='ascending'?noon-half:noon,def.direction==='ascending'?noon:noon+half,def.degrees,def.direction,loc);
72
+ if(def.op==='altitude_drop')return crossing(noon,noon+half,altitude(noon,loc)-def.degrees,'descending',loc);
73
  if(def.op==='shadow'){
74
  const peak=altitude(noon,loc);if(peak<=0)return null;
75
  const target=Math.atan(1/(def.factor+1/Math.tan(peak*rad)))/rad;
76
+ return crossing(noon,noon+half,target,'descending',loc);
77
  }
78
  return null;
79
  };
80
  Object.entries(defs).forEach(([id,def])=>{out[id]=basic(id,def)});
81
+ const nextNoon=maximum(day+D,loc);
82
+ for(let pass=0;pass<need('dependency_passes');pass++)Object.entries(defs).forEach(([id,def])=>{
83
  if(def.op==='alias')out[id]=out[def.boundary]??null;
84
  if(def.op==='next_day'){
85
+ const target=defs[def.boundary];out[id]=target?.op==='altitude'?crossing(nextNoon-half,nextNoon,target.degrees,target.direction,loc):null;
86
  }
87
  if(def.op==='rule_start'){
88
  const rule=worship.rules.find(r=>r.id===def.rule_id),choice=rule?.madhhab?.[madhhab];
 
90
  }
91
  if(def.op==='fraction')out[id]=out[def.from]!=null&&out[def.to]!=null?out[def.from]+def.fraction*(out[def.to]-out[def.from]):null;
92
  });
93
+ // Boundaries that could not be solved (for example twilight at high latitude) are reported, never invented.
94
+ const unresolved=Object.keys(defs).filter(id=>out[id]==null);
95
+ return {day,boundaries:out,solar_transition:transition,unresolved,warnings};
96
  }
97
+ // options.ruleActive(rule, dayStamp): decides rules with a Hijri calendar_rule (the engine itself
98
+ // resolves weekly/daily rules). options.displayOverrides: {estimateId:{before_minutes}} accepted only
99
+ // when the value is listed in that estimate's allowed_before_minutes.
100
  function periods(days,profile,worship,madhhab,styles,scheduleBindings=null,options={}){
101
+ const {ruleActive=null,displayOverrides=null}=options||{};
102
+ const lookup=(id,rule)=>id?(worship.display_estimates?.[id]||rule.window_display_estimates?.[id]||null):null;
103
+ const beforeMinutes=(id,estimate)=>{
104
+ const want=displayOverrides?.[id]?.before_minutes,allowed=estimate.allowed_before_minutes;
105
+ return Number.isFinite(want)&&Array.isArray(allowed)&&allowed.includes(want)?want:Number(estimate.before_minutes||0);
 
 
 
 
 
106
  };
107
+ const out=[];
108
  for(const d of days){const b=d.boundaries;
109
  for(const rule of worship.rules){
110
  const state=styles.category_states[rule.category];if(!state)continue;
111
+ const calendar=rule.calendar_rule;
112
+ if(calendar){
113
+ if(calendar.type==='weekly'){if(calendar.weekday!==WEEKDAYS[new Date(d.day).getUTCDay()])continue;}
114
+ else if(calendar.type!=='daily'&&!(typeof ruleActive==='function'&&ruleActive(rule,d.day)))continue;
115
+ }
116
  const choice=rule.madhhab?.[madhhab];const common={...rule.common,...(typeof choice==='object'?choice:{}),...profile.rule_time_bindings?.[rule.id]};
117
  if(scheduleBindings&&d.methodUnavailable&&state!==styles.category_states.solar_prayer_restriction)continue;
118
  const scheduled=scheduleBindings?.[rule.id];const windows=scheduled?[scheduled]:(typeof choice==='object'&&choice.windows)||rule.windows||[common];
119
  for(const w of windows){
120
+ const weekday=WEEKDAYS[new Date(d.day).getUTCDay()];
121
  if((w.exclude_weekdays||common.exclude_weekdays||[]).includes(weekday))continue;
122
  const requires=w.requires||common.requires||rule.trigger?.requires;
123
  const activation=w.activation||common.activation||(requires?'after_prayer':'boundary');
 
125
  // Preserve the chosen school's outer limit, not a fabricated universal end.
126
  const endId=w.end||common.end||w.outer_end||common.outer_end||w.necessity_end||common.necessity_end||rule.trigger?.end;
127
  let start=b[startId],end=b[endId];
128
+ const estimateId=w.display_estimate_id,estimate=lookup(estimateId,rule);
129
+ if(estimate){const center=b[estimate.boundary];if(center!=null){start=center-beforeMinutes(estimateId,estimate)*MIN;end=center+Number(estimate.after_minutes||0)*MIN;}}
130
+ const endEstimateId=w.display_end_estimate_id||common.display_end_estimate_id,endEstimate=lookup(endEstimateId,rule);
131
+ if(endEstimate){const center=b[endEstimate.boundary];if(center!=null)end=center-beforeMinutes(endEstimateId,endEstimate)*MIN;}
132
  if(start!=null&&end!=null&&end>=start){
133
  const row={id:rule.id,start,end,point:!estimate&&!endEstimate&&(end===start||Boolean(profile.boundaries[endId]?.transition_only)),startId,endId,state,rule,day:d.day,calculationMethod:scheduled?d.calculationMethod:null,conditional:rule.category==='prayer_restriction'||activation==='after_prayer'||Boolean(requires),requires,activation,displayEstimate:estimate||null,displayEndEstimate:endEstimate||null};out.push(row);
134
  for(const binding of styles.preference_bindings||[]){
 
144
  }
145
  return out;
146
  }
147
+ api=Object.freeze({ready,configure,position,altitude,maximum,transitTransition,crossing,day,periods});
148
+ return api;
149
  })();
validate_registries.js CHANGED
@@ -1,87 +1,71 @@
1
  #!/usr/bin/env node
2
- /* فحص الإحالات المتقاطعة بين fiqh_time_registry وworship_fiqh_registry وsky_simulation_registry.
3
- الاستخدام: node validate_registries.js [مجلد السجلات] (يخرج برمز 1 عند وجود أخطاء) */
4
- const fs = require("fs"), path = require("path");
5
- const dir = process.argv[2] || ".";
6
- const read = (name) => JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
7
- const time = read("fiqh_time_registry.json"), worship = read("worship_fiqh_registry.json"), sky = read("sky_simulation_registry.json");
8
  const errors = [], warnings = [];
9
  const err = (m) => errors.push(m), warn = (m) => warnings.push(m);
10
 
 
 
11
  const profile = time.calculation_profile;
12
- const docIds = new Set(time.boundaries.map((b) => b.id));
13
- const opIds = new Set(Object.keys(profile.boundaries));
14
- const known = new Set([...docIds, ...opIds]);
15
- const relationIds = new Set(Object.keys(profile.relation_bindings || {}));
16
- const dup = (list, label) => { const s = new Set(); list.forEach((x) => { if (s.has(x)) err(`${label}: معرّف مكرر ${x}`); s.add(x); }); };
17
 
18
- dup(time.boundaries.map((b) => b.id), "fiqh_time.boundaries");
19
- dup(worship.rules.map((r) => r.id), "worship.rules");
20
-
21
- /* fiqh_time: الترتيب والنوافذ المشتقة */
22
- time.ordering.forEach((id) => { if (!docIds.has(id)) err(`ordering: ${id} غير معرّف في boundaries`); });
23
- time.boundaries.forEach((b) => { if (!time.ordering.includes(b.id)) warn(`boundaries: ${b.id} غير موجود في ordering`); });
24
- time.derived_windows.forEach((w) => [w.start, w.end].forEach((id) => { if (!docIds.has(id)) err(`derived_windows ${w.id}: ${id} غير معرّف`); }));
25
- time.boundaries.forEach((b) => { if (b.source === "astronomy_engine" && !opIds.has(b.id) && !(time.calculation_profile.observational_limits && b.id in time.calculation_profile.observational_limits)) warn(`${b.id}: مصدره astronomy_engine بلا op في calculation_profile.boundaries (سيبقى null دائمًا)`); });
26
- Object.entries(profile.boundaries).forEach(([id, def]) => {
27
- if (def.op === "alias" && !known.has(def.boundary)) err(`op alias ${id}: الهدف ${def.boundary} غير معرّف`);
28
- if (def.op === "next_day" && !opIds.has(def.boundary)) err(`op next_day ${id}: الهدف ${def.boundary} بلا op`);
29
- if (def.op === "fraction") [def.from, def.to].forEach((x) => { if (!opIds.has(x)) err(`op fraction ${id}: ${x} بلا op`); });
30
- if (def.op === "rule_start" && !worship.rules.some((r) => r.id === def.rule_id)) err(`op rule_start ${id}: القاعدة ${def.rule_id} غير موجودة`);
31
- if (def.op === "shadow" && !Number.isFinite(def.factor)) err(`op shadow ${id}: factor مفقود`);
32
- if (def.op === "altitude" && !(Number.isFinite(def.degrees) && ["ascending", "descending"].includes(def.direction))) err(`op altitude ${id}: degrees/direction غير صالحة`);
33
- });
34
- Object.entries(profile.relation_bindings || {}).forEach(([k, v]) => [].concat(v).forEach((id) => { if (!known.has(id)) err(`relation_bindings.${k}: ${id} غير معرّف`); }));
35
- Object.entries(profile.notice_runtime.trigger_bindings).forEach(([k, v]) => { if (!known.has(v.boundary)) err(`trigger_bindings.${k}: ${v.boundary} غير معرّف`); });
36
- Object.entries(profile.rule_time_bindings || {}).forEach(([k]) => { if (!worship.rules.some((r) => r.id === k)) warn(`rule_time_bindings.${k}: لا توجد قاعدة بهذا المعرّف`); });
37
- if (!opIds.has(time.daily_cycle.anchor)) {
38
- const fb = time.daily_cycle.anchor_fallback;
39
- if (fb && opIds.has(fb)) { /* مغطّى بالاحتياط */ }
40
- else warn(`daily_cycle.anchor = ${time.daily_cycle.anchor} لا يملك op ولا anchor_fallback صالحًا، فقيمته null دائمًا`);
41
  }
 
 
 
 
42
 
43
- /* worship: كل حدّ مُشار إليه */
44
- const FIELDS = ["start", "end", "outer_end", "preferred_end", "allowed_to", "necessity_end", "display_start"];
45
- const estimates = new Set();
46
- const checkBoundary = (id, where) => { if (id && !known.has(id)) err(`${where}: ${id} غير معرّف`); };
47
- const checkObj = (o, where) => {
48
- if (!o || typeof o !== "object") return;
49
- FIELDS.forEach((f) => checkBoundary(o[f], `${where}.${f}`));
50
- [o.start_relation, o.end_relation].forEach((r) => { if (r && !relationIds.has(r) && !documentedDescriptive) warn(`${where}: العلاقة ${r} ليست في relation_bindings (علاقة وصفية فقط؟)`); });
51
- (o.windows || []).forEach((w, i) => { checkObj(w, `${where}.windows[${i}]`); if (w.display_estimate_id) estimates.add(w.display_estimate_id); });
52
- };
53
- let documentedDescriptive = false;
54
- worship.rules.forEach((r) => {
55
- documentedDescriptive = Boolean(r.engine_note_ar);
56
- checkObj(r.common, `${r.id}.common`);
57
- Object.entries(r.madhhab || {}).forEach(([m, c]) => {
58
- if (!worship.madhhabs[m]) err(`${r.id}: مذهب ${m} غير معرّف في madhhabs`);
59
- checkObj(c, `${r.id}.${m}`);
60
- });
61
- Object.keys(worship.madhhabs).forEach((m) => { if (r.madhhab && !r.madhhab[m]) warn(`${r.id}: لا توجد قيمة للمذهب ${m}`); });
62
- (r.sources || []).forEach((s) => { if (!worship.sources[s]) err(`${r.id}: المصدر ${s} غير موجود`); });
63
- if (r.window_display_estimates) Object.entries(r.window_display_estimates).forEach(([id, e]) => { checkBoundary(e.boundary, `estimate ${id}`); if (e.source_id && !worship.sources[e.source_id]) err(`estimate ${id}: المصدر ${e.source_id} غير موجود`); });
64
- });
65
- estimates.forEach((id) => { if (!worship.rules.some((r) => r.window_display_estimates && r.window_display_estimates[id]) && !(worship.display_estimates || {})[id]) err(`display_estimate_id ${id} بلا تعريف`); });
66
- (worship.required_time_engine_outputs || []).forEach((o) => { if (!opIds.has(o.id)) err(`required_time_engine_outputs: ${o.id} بلا op`); else if (o.factor !== profile.boundaries[o.id].factor) err(`required_time_engine_outputs ${o.id}: العامل لا يطابق op`); });
67
- Object.values(profile.madhhab_engine_bindings || time.prayer_calculation.madhhab_engine_bindings).forEach(() => {});
68
- Object.keys(worship.madhhabs).forEach((m) => { if (!time.prayer_calculation.madhhab_engine_bindings[m]) err(`madhhab_engine_bindings: لا يوجد ربط للمذهب ${m}`); });
69
- Object.keys(time.prayer_calculation.madhhab_engine_bindings).forEach((m) => { if (!worship.madhhabs[m] && !(worship.madhhab_variants || {})[m]) warn(`madhhab_engine_bindings.${m}: ليس مذهبًا ولا نسخة في worship`); });
70
- Object.entries(worship.madhhab_variants || {}).forEach(([v, def]) => {
71
- if (!worship.madhhabs[def.base]) err(`madhhab_variants.${v}: الأساس ${def.base} غير معرّف`);
72
- Object.entries(def.overrides || {}).forEach(([rid, o]) => { if (!worship.rules.some((r) => r.id === rid)) err(`madhhab_variants.${v}: القاعدة ${rid} غير موجودة`); checkObj(o, `variant ${v}.${rid}`); });
73
- if (!time.prayer_calculation.madhhab_engine_bindings[v]) err(`madhhab_variants.${v}: لا يوجد ربط في madhhab_engine_bindings`);
74
- });
75
- /* منع التناقض بين علَم التحقق والواقع */
76
- const fixedMinutes = JSON.stringify(worship).match(/"(before|after)_minutes"\s*:\s*[1-9]/);
77
- if (worship.validation.no_fixed_solar_minute_offsets === true && fixedMinutes && !(worship.validation.display_only_estimates || []).length) warn("validation.no_fixed_solar_minute_offsets=true لكن توجد دقائق ثابتة في display_estimates ولم تُعلَن display_only_estimates");
78
-
79
- /* sky */
80
- ["sky_color_model", "objects"].forEach((k) => { if (!sky[k]) err(`sky: المفتاح ${k} مفقود`); });
81
- const stops = (sky.sky_color_model || {}).gradient_stops || [];
82
- stops.forEach((s, i) => { if (i && !(s.at > stops[i - 1].at)) err(`sky.gradient_stops: الترتيب غير تصاعدي عند ${s.at}`); if (!/^#[0-9a-fA-F]{6}$/.test(s.color)) err(`sky.gradient_stops: لون غير صالح ${s.color}`); });
83
 
84
- console.log(`أخطاء: ${errors.length} | تحذيرات: ${warnings.length}`);
85
- errors.forEach((e) => console.log("خطأ ", e));
86
- warnings.forEach((w) => console.log("تحذير ", w));
87
  process.exit(errors.length ? 1 : 0);
 
1
  #!/usr/bin/env node
2
+ 'use strict';
3
+ /* مدقّق اتساق بين fiqh_time_registry و worship_fiqh_registry (ومحاكاة قرارات solar_engine.js).
4
+ الاستعمال: node validate_registries.js [المجلد] — يُرجع رمز خروج 1 عند وجود أخطاء. */
5
+ const fs = require('fs'), path = require('path');
6
+ const dir = process.argv[2] || '.';
7
+ const load = (name) => JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
8
  const errors = [], warnings = [];
9
  const err = (m) => errors.push(m), warn = (m) => warnings.push(m);
10
 
11
+ const time = load('fiqh_time_registry.json');
12
+ const worship = load('worship_fiqh_registry.json');
13
  const profile = time.calculation_profile;
14
+ const boundaryIds = new Set(time.boundaries.map((b) => b.id));
15
+ const aliasIds = new Set(Object.keys(time.aliases || {}));
16
+ const known = (id) => boundaryIds.has(id) || aliasIds.has(id);
 
 
17
 
18
+ /* ---- fiqh_time_registry ---- */
19
+ const seen = new Set();
20
+ for (const b of time.boundaries) { if (seen.has(b.id)) err(`time: معرّف حد مكرر ${b.id}`); seen.add(b.id); }
21
+ for (const id of time.ordering) if (!known(id)) err(`time.ordering: ${id} غير معرّف`);
22
+ for (const b of time.boundaries) if (!time.ordering.includes(b.id)) warn(`time.ordering: الحد ${b.id} غير مرتّب`);
23
+ for (const [id, def] of Object.entries(profile.boundaries)) {
24
+ if (!known(id)) err(`profile.boundaries: ${id} غير موجود في boundaries أو aliases`);
25
+ for (const key of ['boundary', 'from', 'to']) if (def[key] && !known(def[key]) && !profile.boundaries[def[key]]) err(`profile.boundaries.${id}.${key}: ${def[key]} غير معرّف`);
26
+ if (def.op === 'rule_start' && !worship.rules.some((r) => r.id === def.rule_id)) err(`profile.boundaries.${id}: القاعدة ${def.rule_id} غير موجودة في worship`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
+ for (const [k, v] of Object.entries(profile.relation_bindings || {})) for (const id of [].concat(v)) if (!known(id) && !profile.boundaries[id]) err(`relation_bindings.${k}: ${id} غير معرّف`);
29
+ for (const rid of Object.keys(profile.rule_time_bindings || {})) if (!worship.rules.some((r) => r.id === rid)) err(`rule_time_bindings: القاعدة ${rid} غير موجودة`);
30
+ for (const key of ['time_tolerance_ms', 'derivative_sample_ms', 'scan_step_ms', 'bisect_iterations', 'maximum_iterations', 'transit_iterations', 'noon_bracket_hours', 'half_day_hours', 'dependency_passes', 'longitude_degrees_per_hour'])
31
+ if (!(Number(profile.solar_solver?.[key]) > 0)) err(`solar_solver.${key} مفقود أو غير موجب`);
32
 
33
+ /* ---- worship_fiqh_registry ---- */
34
+ const ruleIds = new Set();
35
+ for (const r of worship.rules) { if (ruleIds.has(r.id)) err(`worship: قاعدة مكررة ${r.id}`); ruleIds.add(r.id); }
36
+ const estimateIds = new Set(Object.keys(worship.display_estimates || {}));
37
+ for (const r of worship.rules) for (const id of Object.keys(r.window_display_estimates || {})) estimateIds.add(id);
38
+ const refKeys = ['start', 'end', 'display_start', 'outer_end', 'necessity_end', 'preferred_end', 'allowed_to'];
39
+ const checkRef = (where, id) => { if (id && !known(id) && !profile.boundaries[id]) err(`${where}: الحد ${id} غير معرّف في fiqh_time_registry`); };
40
+ for (const rule of worship.rules) {
41
+ for (const sid of rule.sources || []) if (!worship.sources[sid]) err(`rule ${rule.id}: المصدر ${sid} غير معرّف`);
42
+ const scan = (where, o) => { if (!o || typeof o !== 'object') return; for (const k of refKeys) checkRef(`${where}.${k}`, o[k]); for (const k of ['display_estimate_id', 'display_end_estimate_id']) if (o[k] && !estimateIds.has(o[k])) err(`${where}.${k}: التقدير ${o[k]} غير معرّف`); };
43
+ scan(`rule ${rule.id}.common`, rule.common);
44
+ for (const [m, choice] of Object.entries(rule.madhhab || {})) {
45
+ if (!worship.madhhabs[m]) err(`rule ${rule.id}: مذهب غير معرّف ${m}`);
46
+ scan(`rule ${rule.id}.${m}`, choice);
47
+ for (const w of (typeof choice === 'object' && choice.windows) || []) scan(`rule ${rule.id}.${m}.window`, w);
48
+ }
49
+ /* محاكاة قرار solar_engine.periods() لكل مذهب: هل ينتج صفًا؟ */
50
+ const needsCalendar = Boolean(rule.calendar_rule);
51
+ for (const m of Object.keys(worship.madhhabs)) {
52
+ const choice = rule.madhhab?.[m];
53
+ const common = { ...rule.common, ...(typeof choice === 'object' ? choice : {}), ...(profile.rule_time_bindings?.[rule.id] || {}) };
54
+ const windows = (typeof choice === 'object' && choice.windows) || rule.windows || [common];
55
+ for (const w of windows) {
56
+ const startId = w.display_start || common.display_start || w.start || common.start || rule.trigger?.start || profile.relation_bindings?.[w.start_relation || common.start_relation];
57
+ const endId = w.end || common.end || w.outer_end || common.outer_end || w.necessity_end || common.necessity_end || rule.trigger?.end;
58
+ const estimate = estimateIds.has(w.display_estimate_id);
59
+ if (!estimate && (!startId || !endId)) err(`rule ${rule.id} [${m}]: لن يُنتج صفًا (بداية=${startId || '∅'} ، نهاية=${endId || '∅'})${needsCalendar ? ' — قاعدة تقويمية' : ''}`);
60
+ }
61
+ }
62
+ }
63
+ for (const out of worship.required_time_engine_outputs || []) if (!profile.boundaries[out.id] && !known(out.id)) err(`required_time_engine_outputs: ${out.id} غير معرّف`);
64
+ const used = new Set();
65
+ for (const rule of worship.rules) JSON.stringify(rule, (k, v) => { if (refKeys.includes(k) && typeof v === 'string') used.add(v); return v; });
66
+ for (const id of used) if (known(id) && !(worship.required_time_engine_outputs || []).some((o) => o.id === id) && !profile.boundaries[id] && !aliasIds.has(id) && !time.boundaries.some((b) => b.id === id && b.source !== 'astronomy_engine')) warn(`الحد ${id} مستخدم ولا يظهر في required_time_engine_outputs`);
 
 
 
 
 
 
67
 
68
+ console.log(`أخطاء: ${errors.length} | تحذيرات: ${warnings.length}`);
69
+ errors.forEach((e) => console.log(' ✗ ' + e));
70
+ warnings.forEach((w) => console.log(' ! ' + w));
71
  process.exit(errors.length ? 1 : 0);
worship_event_engine.js CHANGED
@@ -1,9 +1,6 @@
1
  'use strict';
2
 
3
  // Pure registry scheduler. Solar geometry and religious rules remain in their owners.
4
- // v2: eclipse search is cached on a day-aligned window (the old key changed every call), the cache is
5
- // per event, only real eclipse types are searched, penumbral/excluded subtypes are skipped, and the
6
- // visibility altitude comes from the registry's visibility_rule.
7
  globalThis.WorshipEventEngine = (() => {
8
  const DAY=86400000,MINUTE=60000;
9
  const civil=stamp=>{const d=new Date(stamp);return new Date(d.getUTCFullYear(),d.getUTCMonth(),d.getUTCDate(),12);};
@@ -62,42 +59,46 @@ globalThis.WorshipEventEngine = (() => {
62
  return notices.sort((a,b)=>a.at-b.at||b.priority-a.priority);
63
  }
64
  const eclipseCache=new Map();
65
- const ECLIPSE_FUNCTIONS=['SearchLocalSolarEclipse','SearchLunarEclipse'],ECLIPSE_TYPES=['solar_eclipse','lunar_eclipse'];
66
- const isEclipseType=type=>ECLIPSE_FUNCTIONS.includes(type.engine_call?.function)||ECLIPSE_TYPES.includes(type.id);
67
- function visibleInterval(A,body,observer,start,end,minAltitude=0){
68
- const altitude=t=>{const d=new Date(t),eq=A.Equator(body,d,observer,true,true);return A.Horizon(d,observer,eq.ra,eq.dec,'normal').altitude;};
69
  let first=start;
70
- // Conservative center-above-threshold visibility, solved by the same engine.
71
  if(altitude(first)<=minAltitude){const rise=A.SearchAltitude(body,observer,1,new Date(start),(end-start)/DAY,minAltitude);if(!rise)return null;first=rise.date.getTime();}
72
- const set=A.SearchAltitude(body,observer,-1,new Date(first+1000),(end-first)/DAY,minAltitude);
73
  const last=set?Math.min(end,set.date.getTime()):end;
74
  return last>first?{start:first,end:last}:null;
75
  }
 
 
76
  function eclipses({astronomy,event,location,start,end,A=globalThis.Astronomy}){
77
- if(!A||!location||!event||event.enabled===false||astronomy.enabled===false)return [];
78
- // Day-aligned window so repeated (e.g. per-minute) calls reuse the same computation.
79
- const from=Math.floor(start/DAY)*DAY-DAY,to=Math.ceil(end/DAY)*DAY+DAY;
80
- const cacheKey=JSON.stringify([event.id,location.lat,location.lng,location.height||0,from,to]);let occurrences=eclipseCache.get(cacheKey);
 
 
81
  if(!occurrences){
82
  occurrences=[];const observer=new A.Observer(location.lat,location.lng,location.height||0);
83
- for(const type of astronomy.event_types.filter(t=>t.religious_relation?.practice_id===event.id&&t.enabled!==false&&isEclipseType(t))){
 
 
 
84
  try{
85
- const solar=type.id==='solar_eclipse',excluded=type.religious_relation?.excluded_subtypes||[];
86
- const minAltitude=Number(type.religious_relation?.visibility_rule?.min_altitude_deg??0);
87
- let found=solar?A.SearchLocalSolarEclipse(new Date(from),observer):A.SearchLunarEclipse(new Date(from));
88
- for(let i=0;i<8;i++){
89
- const peak=(solar?found.peak.time:found.peak).date.getTime();if(peak>to)break;
90
- const kind=String(found.kind||'').toLowerCase();
91
- if(!excluded.includes(kind)){
92
  const begin=solar?found.partial_begin.time.date.getTime():peak-found.sd_partial*MINUTE;
93
  const finish=solar?found.partial_end.time.date.getTime():peak+found.sd_partial*MINUTE;
94
- if(finish>=from&&finish>begin){const visible=visibleInterval(A,solar?'Sun':'Moon',observer,begin,finish,minAltitude);if(visible)occurrences.push({typeId:type.id,peak,...visible});}
95
  }
96
  found=solar?A.NextLocalSolarEclipse(found.peak.time,observer):A.NextLunarEclipse(found.peak);
97
  }
98
  }catch{/* No invented eclipse when ephemeris calculation fails. */}
99
  }
100
- eclipseCache.set(cacheKey,occurrences);if(eclipseCache.size>8)eclipseCache.delete(eclipseCache.keys().next().value);
101
  }
102
  const notices=[];
103
  for(const occurrence of occurrences){
 
1
  'use strict';
2
 
3
  // Pure registry scheduler. Solar geometry and religious rules remain in their owners.
 
 
 
4
  globalThis.WorshipEventEngine = (() => {
5
  const DAY=86400000,MINUTE=60000;
6
  const civil=stamp=>{const d=new Date(stamp);return new Date(d.getUTCFullYear(),d.getUTCMonth(),d.getUTCDate(),12);};
 
59
  return notices.sort((a,b)=>a.at-b.at||b.priority-a.priority);
60
  }
61
  const eclipseCache=new Map();
62
+ // Geometric centre altitude: the same convention as Astronomy.SearchAltitude, so the horizon test
63
+ // and the rise/set search agree (the previous version mixed refracted and geometric altitudes).
64
+ function visibleInterval(A,body,observer,start,end,minAltitude,epsilon){
65
+ const altitude=t=>{const d=new Date(t),eq=A.Equator(body,d,observer,true,true);return A.Horizon(d,observer,eq.ra,eq.dec,null).altitude;};
66
  let first=start;
 
67
  if(altitude(first)<=minAltitude){const rise=A.SearchAltitude(body,observer,1,new Date(start),(end-start)/DAY,minAltitude);if(!rise)return null;first=rise.date.getTime();}
68
+ const set=A.SearchAltitude(body,observer,-1,new Date(first+epsilon),(end-first)/DAY,minAltitude);
69
  const last=set?Math.min(end,set.date.getTime()):end;
70
  return last>first?{start:first,end:last}:null;
71
  }
72
+ // Scan limits come from astronomy.defaults.eclipse_scan; the visibility threshold and the excluded
73
+ // eclipse kinds come from each event type's religious_relation. Missing data means no notice, never a guess.
74
  function eclipses({astronomy,event,location,start,end,A=globalThis.Astronomy}){
75
+ const scan=astronomy?.defaults?.eclipse_scan;
76
+ const complete=scan&&['max_occurrences','lookback_days','cache_limit','epsilon_ms'].every(key=>Number(scan[key])>0);
77
+ if(!A||!location||!event||event.enabled===false||astronomy.enabled===false||!complete)return [];
78
+ // Snap to whole days so the cache survives the per-minute calls of the notice loop.
79
+ const from=Math.floor(start/DAY)*DAY,to=Math.ceil(end/DAY)*DAY;
80
+ const cacheKey=JSON.stringify([location,from,to]);let occurrences=eclipseCache.get(cacheKey);
81
  if(!occurrences){
82
  occurrences=[];const observer=new A.Observer(location.lat,location.lng,location.height||0);
83
+ for(const type of astronomy.event_types.filter(t=>t.religious_relation?.practice_id===event.id&&t.enabled!==false)){
84
+ const relation=type.religious_relation,rule=relation.visibility_rule;
85
+ if(!rule||!Number.isFinite(rule.min_altitude_deg))continue;
86
+ const excluded=(relation.excluded_subtypes||[]).map(kind=>String(kind).toLowerCase());
87
  try{
88
+ const solar=type.id==='solar_eclipse',origin=new Date(from-scan.lookback_days*DAY);
89
+ let found=solar?A.SearchLocalSolarEclipse(origin,observer):A.SearchLunarEclipse(origin);
90
+ for(let i=0;i<scan.max_occurrences;i++){
91
+ const peak=(solar?found.peak.time:found.peak).date.getTime();if(peak>to+DAY)break;
92
+ if(!excluded.includes(String(found.kind).toLowerCase())){
 
 
93
  const begin=solar?found.partial_begin.time.date.getTime():peak-found.sd_partial*MINUTE;
94
  const finish=solar?found.partial_end.time.date.getTime():peak+found.sd_partial*MINUTE;
95
+ if(finish>=from&&finish>begin){const visible=visibleInterval(A,solar?'Sun':'Moon',observer,begin,finish,rule.min_altitude_deg,scan.epsilon_ms);if(visible)occurrences.push({typeId:type.id,kind:String(found.kind).toLowerCase(),peak,...visible});}
96
  }
97
  found=solar?A.NextLocalSolarEclipse(found.peak.time,observer):A.NextLunarEclipse(found.peak);
98
  }
99
  }catch{/* No invented eclipse when ephemeris calculation fails. */}
100
  }
101
+ eclipseCache.set(cacheKey,occurrences);if(eclipseCache.size>scan.cache_limit)eclipseCache.delete(eclipseCache.keys().next().value);
102
  }
103
  const notices=[];
104
  for(const occurrence of occurrences){
worship_fiqh_registry.json CHANGED
@@ -1,618 +1,651 @@
1
  {
2
- "schema_name": "islamic_clock_worship_fiqh_registry",
3
- "schema_version": "1.3.0",
4
- "registry_id": "worship_fiqh",
5
- "project": "ساعة المسلم / الخط الزمني الروحي",
6
- "language": "ar",
7
- "direction": "rtl",
8
- "status": "researched_v2_madhhab_restriction_windows",
9
- "scope_ar": "أحكام العبادات المرتبطة بالزمن فقط مع الخلاف بين المذاهب الأربعة. لا يملك الحساب الفلكي أو الألوان أو الأيقونات أو منبهات المستخدم.",
10
- "architecture_contract": {
11
- "time_geometry_owner": "fiqh_time_registry.json",
12
- "worship_event_owner": "islamic_events_registry.json",
13
- "visual_owner": "visual_states_registry.json",
14
- "user_alarm_owner": "user_time_markers_registry.json",
15
- "knowledge_owner": "contextual_knowledge_registry.json",
16
- "no_fixed_solar_minutes": true
17
- },
18
- "madhhabs": {
19
- "hanafi": "الحنفي",
20
- "maliki": "المالكي",
21
- "shafii": "الشافعي",
22
- "hanbali": "الحنبلي"
23
- },
24
- "rules": [
25
- {
26
- "id": "fajr",
27
- "category": "obligatory_prayer",
28
- "common": {
29
- "start": "true_dawn",
30
- "end": "sunrise"
31
- },
32
- "note": "أصل النافذة مشترك؛ درجات الفضيلة والاختيار تفصيل مذهبي."
33
- },
34
- {
35
- "id": "dhuhr",
36
- "category": "obligatory_prayer",
37
- "common": {
38
- "start": "zawal"
39
- },
40
- "madhhab": {
41
- "hanafi": {
42
- "end": "asr_shadow_2",
43
- "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: asr_shadow_1"
44
- },
45
- "maliki": {
46
- "end": "asr_shadow_1"
47
- },
48
- "shafii": {
49
- "end": "asr_shadow_1"
50
- },
51
- "hanbali": {
52
- "end": "asr_shadow_1"
53
- }
54
- },
55
- "sources": [
56
- "hanafi_asr",
57
- "asr_comparison"
58
- ]
59
- },
60
- {
61
- "id": "asr",
62
- "category": "obligatory_prayer",
63
- "common": {
64
- "end": "sunset"
65
- },
66
- "madhhab": {
67
- "hanafi": {
68
- "start": "asr_shadow_2",
69
- "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: asr_shadow_1",
70
- "late": "يكره التأخير إلى تغير/اصفرار الشمس بلا عذر"
71
- },
72
- "maliki": {
73
- "start": "asr_shadow_1",
74
- "note": "يفصل بين الاختياري والضروري"
75
- },
76
- "shafii": {
77
- "start": "asr_shadow_1",
78
- "note": "توجد مراتب للوقت"
79
- },
80
- "hanbali": {
81
- "start": "asr_shadow_1",
82
- "note": "وقت اختيار ثم ضرورة إلى الغروب"
83
- }
84
- },
85
- "sources": [
86
- "hanafi_asr",
87
- "asr_comparison"
88
- ]
89
- },
90
- {
91
- "id": "maghrib",
92
- "category": "obligatory_prayer",
93
- "common": {
94
- "start": "sunset"
95
- },
96
- "madhhab": {
97
- "hanafi": {
98
- "end": "isha_entry_hanafi"
99
- },
100
- "maliki": {
101
- "end": "red_twilight_end"
102
- },
103
- "shafii": {
104
- "end": "red_twilight_end",
105
- "note": "يوجد تفصيل قديم/جديد في المذهب؛ تحفظ الروايات ولا تختزل"
106
- },
107
- "hanbali": {
108
- "end": "red_twilight_end"
109
- }
110
- },
111
- "sources": [
112
- "maghrib_shafii",
113
- "isha_twilight"
114
- ]
115
- },
116
- {
117
- "id": "isha_entry",
118
- "category": "obligatory_prayer",
119
- "madhhab": {
120
- "hanafi": {
121
- "start": "white_twilight_end",
122
- "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: red_twilight_end"
123
- },
124
- "maliki": {
125
- "start": "red_twilight_end"
126
- },
127
- "shafii": {
128
- "start": "red_twilight_end"
129
- },
130
- "hanbali": {
131
- "start": "red_twilight_end"
132
- }
133
- },
134
- "sources": [
135
- "isha_twilight"
136
- ]
137
- },
138
- {
139
- "id": "isha_periods",
140
- "category": "obligatory_prayer",
141
- "madhhab": {
142
- "hanafi": {
143
- "outer_end": "next_true_dawn",
144
- "note": "داخل المذهب مراتب فضيلة قبل ذلك"
145
- },
146
- "maliki": {
147
- "preferred_end": "first_third_of_night",
148
- "outer_end": "next_true_dawn",
149
- "note": "تفصيل الاختياري/الضروري يحفظ في profile المذهب"
150
- },
151
- "shafii": {
152
- "preferred_end": "first_third_of_night",
153
- "outer_end": "next_true_dawn"
154
- },
155
- "hanbali": {
156
- "preferred_end": "first_third_of_night",
157
- "allowed_to": "islamic_midnight",
158
- "necessity_end": "next_true_dawn"
159
- }
160
- },
161
- "sources": [
162
- "isha_end",
163
- "isha_end_detail"
164
- ]
165
- },
166
- {
167
- "id": "duha",
168
- "category": "voluntary_prayer",
169
- "common": {
170
- "start": "sunrise_clear",
171
- "end": "solar_noon",
172
- "ruling": "recommended",
173
- "best_time": "when_sun_is_high_and_heat_has_increased"
174
- },
175
- "madhhab": {
176
- "hanafi": "recommended",
177
- "maliki": "recommended",
178
- "shafii": "emphasized_sunnah",
179
- "hanbali": "recommended"
180
- },
181
- "implementation": "لا +15 دقيقة بعد الشروق ولا -5 قبل الزوال؛ الحدود من الهندسة الشمسية.",
182
- "sources": [
183
- "duha"
184
- ]
185
- },
186
- {
187
- "id": "witr",
188
- "category": "night_prayer",
189
- "common": {
190
- "start_relation": "after_isha",
191
- "end": "next_true_dawn",
192
- "preferred": "آخر الليل لمن يثق بالاستيقاظ",
193
- "early_option": "قبل النوم لمن يخشى ألا يستيقظ"
194
- },
195
- "sources": [
196
- "witr"
197
- ]
198
- },
199
- {
200
- "id": "qiyam",
201
- "category": "night_prayer",
202
- "common": {
203
- "start_relation": "after_isha",
204
- "end": "next_true_dawn",
205
- "preferred_window": "last_third_start_to_next_true_dawn"
206
- },
207
- "sources": [
208
- "witr",
209
- "last_third"
210
- ]
211
- },
212
- {
213
- "id": "jumuah",
214
- "category": "obligatory_prayer",
215
- "madhhab": {
216
- "hanafi": {
217
- "start": "zawal",
218
- "end_relation": "dhuhr_end"
219
- },
220
- "maliki": {
221
- "start": "zawal",
222
- "end_relation": "dhuhr_end_with_detail"
223
- },
224
- "shafii": {
225
- "start": "zawal",
226
- "end_relation": "dhuhr_end"
227
- },
228
- "hanbali": {
229
- "start_variant": "before_zawal_allowed",
230
- "preferred_display": "after_zawal",
231
- "end_relation": "asr_entry"
232
- }
233
- },
234
- "sources": [
235
- "jumuah"
236
- ]
237
- },
238
- {
239
- "id": "after_fajr_nafl_restriction",
240
- "category": "prayer_restriction",
241
- "scope": "general_voluntary_prayer",
242
- "display_policy": "show_without_worship_tracking",
243
- "madhhab": {
244
- "hanafi": {
245
- "activation": "boundary",
246
- "start": "true_dawn",
247
- "end": "sunrise",
248
- "exception": "fajr_sunnah_before_obligatory_prayer"
249
- },
250
- "maliki": {
251
- "activation": "boundary",
252
- "start": "true_dawn",
253
- "end": "sunrise",
254
- "exception": "fajr_sunnah_before_obligatory_prayer"
255
- },
256
- "shafii": {
257
- "activation": "after_prayer",
258
- "requires": "fajr_prayer_performed",
259
- "display_start": "fajr",
260
- "end": "sunrise"
261
- },
262
- "hanbali": {
263
- "activation": "boundary",
264
- "start": "true_dawn",
265
- "end": "sunrise",
266
- "exception": "fajr_sunnah_before_obligatory_prayer",
267
- "variant": "رواية: يتعلق النهي بأداء صلاة الفجر"
268
- }
269
- },
270
- "critical": "يختلف بدء النهي: عند الحنفية والمالكية والمشهور عند الحنابلة من طلوع الفجر، وعند الشافعية بعد أداء صلاة الفجر. العرض البرتقالي لا يسجل أداء العبادة.",
271
- "note": "تنتهي هذه الطبقة عند الشروق، ثم تبدأ طبقة النهي الشمسي الأحمر إلى ارتفاع الشمس. الاستثناءات المتعلقة بنوع الصلاة تعرض في تفاصيل الحكم ولا تغيّر الحد الزمني العام بلا دليل خاص.",
272
- "sources": [
273
- "post_prayer_prohibition",
274
- "prohibition_comparative",
275
- "prohibition_trigger_comparative"
276
- ]
277
- },
278
- {
279
- "id": "after_asr_nafl_restriction",
280
- "category": "prayer_restriction",
281
- "scope": "general_voluntary_prayer",
282
- "display_policy": "conditional_window_without_worship_tracking",
283
- "common": {
284
- "activation": "after_prayer",
285
- "requires": "asr_prayer_performed",
286
- "display_start": "asr",
287
- "end": "sunset_approach"
288
- },
289
- "madhhab": {
290
- "hanafi": {
291
- "activation": "after_prayer",
292
- "requires": "asr_prayer_performed"
293
- },
294
- "maliki": {
295
- "activation": "after_prayer",
296
- "requires": "asr_prayer_performed"
297
- },
298
- "shafii": {
299
- "activation": "after_prayer",
300
- "requires": "asr_prayer_performed"
301
- },
302
- "hanbali": {
303
- "activation": "after_prayer",
304
- "requires": "asr_prayer_performed"
305
- }
306
- },
307
- "critical": "لا يبدأ الحكم بمجرد دخول وقت العصر، بل بعد أداء صلاة العصر. ولأن التطبيق لا يتتبع أداء الصلاة، تعرض المسطرة نافذة برتقالية مشروطة من دخول العصر، لا حكمًا محققًا على المستخدم.",
308
- "note": "تنتهي الطبقة المشروطة عند اقتراب الغروب، ثم تظهر طبقة النهي الشمسي الأحمر حتى الغروب. الاستثناءات وذوات الأسباب تختلف بين المذاهب وتبقى في تفاصيل الحكم.",
309
- "sources": [
310
- "post_prayer_prohibition",
311
- "prohibition_comparative"
312
- ]
313
- },
314
- {
315
- "id": "absolute_solar_restrictions",
316
- "category": "solar_prayer_restriction",
317
- "scope": "absolute_solar_windows",
318
- "window_display_estimates": {
319
- "zawal_display_window": {
320
- "boundary": "solar_noon",
321
- "before_minutes": 10,
322
- "after_minutes": 0,
323
- "user_choices_before_minutes": [
324
- 10,
325
- 15
326
- ],
327
- "settings_key": "zawal_before_minutes",
328
- "label_ar": "نافذة عرض قبيل الزوال",
329
- "note_ar": "نافذة عرض فقط قبل الزوال: تبدأ قبل الاستواء بالمقدار المختار وتنتهي عند الاستواء. بعد الزوال يدخل وقت الظهر فلا تمتد النافذة بعده. القيمتان 10 و15 خيارا عرض، وليستا حدًّا فلكيًا ولا تحديدًا شرعيًا للمدة؛ الأبحاث المذكورة تقدّر المنطقة القريبة من الزوال عادةً بنحو 5–10 دقائق.",
330
- "source_id": "zawal_duration_research"
331
- },
332
- "zawal_20_minutes": {
333
- "deprecated_alias_of": "zawal_display_window"
334
- }
335
- },
336
- "madhhab": {
337
- "hanafi": {
338
- "windows": [
339
- {
340
- "start": "sunrise",
341
- "end": "sunrise_clear"
342
- },
343
- {
344
- "start": "solar_noon",
345
- "end": "zawal",
346
- "display_estimate_id": "zawal_display_window"
347
- },
348
- {
349
- "start": "sunset_approach",
350
- "end": "sunset"
351
- }
352
- ],
353
- "variant": "رُوي عن أبي يوسف الترخيص في الاستواء يوم الجمعة، وليس هو العرض الافتراضي للمذهب هنا."
354
- },
355
- "maliki": {
356
- "windows": [
357
- {
358
- "start": "sunrise",
359
- "end": "sunrise_clear"
360
- },
361
- {
362
- "start": "sunset_approach",
363
- "end": "sunset"
364
- }
365
- ],
366
- "note": "لا تعرض نافذة الاستواء إلى الزوال في الاختيار المالكي."
367
- },
368
- "shafii": {
369
- "windows": [
370
- {
371
- "start": "sunrise",
372
- "end": "sunrise_clear"
373
- },
374
- {
375
- "start": "solar_noon",
376
- "end": "zawal",
377
- "exclude_weekdays": [
378
- "friday"
379
- ],
380
- "display_estimate_id": "zawal_display_window"
381
- },
382
- {
383
- "start": "sunset_approach",
384
- "end": "sunset"
385
- }
386
- ],
387
- "note": "يستثنى الاستواء يوم الجمعة من العرض وفق المعتمد المسجل في المصدر المقارن."
388
- },
389
- "hanbali": {
390
- "windows": [
391
- {
392
- "start": "sunrise",
393
- "end": "sunrise_clear"
394
- },
395
- {
396
- "start": "solar_noon",
397
- "end": "zawal",
398
- "display_estimate_id": "zawal_display_window"
399
- },
400
- {
401
- "start": "sunset_approach",
402
- "end": "sunset"
403
- }
404
- ]
405
- }
406
- },
407
- "implementation": "الحدود هندسة شمسية بلا دقائق ثابتة؛ نافذة قبيل الزوال سياسة عرض قبل الاستواء فقط وليست بديلًا عن الحد الفلكي، ويمكن للمستخدم اختيار 10 أو 15 دقيقة.",
408
- "note": "اختيار النوافذ نفسها مذهبي. تعرض نافذة الزوال بمدة عرض احتياطية قبل الزوال فقط (10 افتراضيًا و15 خيار المستخدم)، مع بيان أن الأبحاث المذكورة تقدر المنطقة الأقرب للزوال عادةً بنحو 5–10 دقائق. أما استثناءات أنواع الصلوات كذوات الأسباب والقضاء والجنازة والطواف فلا تختزل في لون زمني عام، وتعرض في تفاصيل الحكم.",
409
- "sources": [
410
- "prohibition_comparative",
411
- "solar_prohibition",
412
- "zawal_duration_research"
413
- ]
414
- },
415
- {
416
- "id": "ramadan_fast",
417
- "category": "obligatory_fasting",
418
- "common": {
419
- "start": "true_dawn",
420
- "end": "sunset"
421
- },
422
- "note": "ثبوت بداية الشهر من سلطة التقويم/الرؤية المعتمدة، لا من هذا الملف.",
423
- "sources": [
424
- "quran_2_187"
425
- ]
426
- },
427
- {
428
- "id": "monday_thursday_fast",
429
- "category": "recommended_fasting",
430
- "common": {
431
- "start": "true_dawn",
432
- "end": "sunset"
433
- },
434
- "note": "اختيار اليوم وجدولته من islamic_events_registry.json."
435
  }
436
- ],
437
- "required_time_engine_outputs": [
438
- {
439
- "id": "asr_shadow_1",
440
- "type": "shadow_factor",
441
- "factor": 1,
442
- "exclude_noon_shadow": true
443
- },
444
- {
445
- "id": "asr_shadow_2",
446
- "type": "shadow_factor",
447
- "factor": 2,
448
- "exclude_noon_shadow": true
449
- },
450
- {
451
- "id": "first_third_of_night",
452
- "type": "derived_night_fraction",
453
- "fraction": "1/3",
454
- "from": "sunset",
455
- "to": "next_true_dawn"
 
 
 
 
 
 
 
 
 
456
  }
457
- ],
458
- "sources": {
459
- "hanafi_asr": {
460
- "title": "نقول حنفية في المثل والمثلين",
461
- "url": "https://islamqa.org/hanafi/darul-iftaa-chicago/271053/"
462
- },
463
- "asr_comparison": {
464
- "title": "الفرق بين الحنفية والجمهور في أول العصر",
465
- "url": "https://islamqa.info/ar/answers/220820"
466
- },
467
- "maghrib_shafii": {
468
- "title": "دائرة الإفتاء الأردنية — وقت المغرب",
469
- "url": "https://aliftaa.jo/fatwa/3399/CatQuestion.aspx?Id=133"
470
- },
471
- "isha_twilight": {
472
- "title": "الموسوعة الفقهية — الشفق ووقت العشاء",
473
- "url": "https://dorar.net/feqhia/846/"
474
- },
475
- "isha_end": {
476
- "title": "وقت خروج العشاء في المذاهب الأربعة",
477
- "url": "https://www.islamweb.org/ar/fatwa/71957/"
478
- },
479
- "isha_end_detail": {
480
- "title": "تفصيل آخر وقت العشاء",
481
- "url": "https://www.alqayim.org/ar/fatwa/1971"
482
- },
483
- "duha": {
484
- "title": "الموسوعة الفقهية — صلاة الضحى",
485
- "url": "https://dorar.net/feqhia/1261/"
486
- },
487
- "witr": {
488
- "title": "الموسوعة الفقهية — أفضل وقت للوتر",
489
- "url": "https://dorar.net/feqhia/1241/"
490
- },
491
- "last_third": {
492
- "title": "صحيح البخاري 1145",
493
- "url": "https://sunnah.com/bukhari:1145"
494
- },
495
- "jumuah": {
496
- "title": "مذاهب العلماء في وقت الجمعة",
497
- "url": "https://islamweb.net/ar/fatwa/23917/"
498
- },
499
- "post_prayer_prohibition": {
500
- "title": "صحيح مسلم 827",
501
- "url": "https://sunnah.com/muslim:827"
502
- },
503
- "prohibition_comparative": {
504
- "title": "بحث مقارن في أوقات النهي",
505
- "url": "https://islamport.com/l/msl/3529/9.htm"
506
- },
507
- "prohibition_trigger_comparative": {
508
- "title": "بحث مقارن في تعلق نهي الفجر بطلوعه أو بأداء الصلاة",
509
- "url": "https://islamport.com/l/msl/3529/17.htm"
510
- },
511
- "solar_prohibition": {
512
- "title": "صحيح مسلم 831",
513
- "url": "https://sunnah.com/muslim:831"
514
- },
515
- "zawal_duration_research": {
516
- "title": "تقدير المدة القريبة من الزوال وبيان أنها غير ثابتة بعشرين دقيقة",
517
- "url": "https://saadalkhathlan.com/scientific-series/lessons/32181"
518
- },
519
- "quran_2_187": {
520
- "title": "البقرة 187",
521
- "reference": "Quran 2:187"
522
  }
 
 
 
 
 
523
  },
524
- "validation": {
525
- "worship_only": true,
526
- "no_colors": true,
527
- "no_emoji": true,
528
- "no_user_alarms": true,
529
- "no_completion_tracking": true,
530
- "no_fixed_solar_minute_offsets": true,
531
- "preserve_madhhab_variants": true,
532
- "proposed_rules_not_consumed_by_periods": true
533
- },
534
- "research_note": "هذه طبقة بيانات للتطبيق وليست فتوى شخصية. حيث لم تكف المصادر لإغلاق تفصيل مذهبي دقيق، أبقي التفصيل معلّقًا بدل اختراع قيمة.",
535
- "changelog": [
536
- "1.3.0: تصحيح نافذة الزوال: كانت ±10 دقائق حول الاستواء وتشمل عشر دقائق بعد دخول وقت الظهر؛ أصبحت قبل الزوال فقط (10 افتراضيًا، 15 خيار المستخدم)",
537
- "1.3.0: معرّف النافذة zawal_display_window وبقاء الاسم القديم كمرادف مهجور",
538
- "1.3.0: أحكام مقترحة غير مفعّلة في proposed_rules حتى توثيق المصادر وربط التفعيل بسجل الأحداث"
539
- ],
540
- "proposed_rules": [
541
- {
542
- "id": "eid_prayer",
543
- "category": "festival_prayer",
544
- "status": "proposed_not_consumed",
545
- "activation": {
546
- "owner": "islamic_events_registry.json",
547
- "note_ar": "تُفعَّل في يومي العيد فقط من سجل الأحداث، ولا تُعرض يوميًا."
548
- },
549
- "common": {
550
- "start": "sunrise_clear",
551
- "end": "zawal"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  },
553
- "madhhab": {
554
- "hanafi": {
555
- "ruling": "wajib"
556
- },
557
- "maliki": {
558
- "ruling": "sunnah_muakkadah"
559
- },
560
- "shafii": {
561
- "ruling": "sunnah_muakkadah"
562
- },
563
- "hanbali": {
564
- "ruling": "fard_kifayah"
565
- }
566
  },
567
- "research_status": "needs_sources",
568
- "needs_visual_state": "festival_prayer",
569
- "note": "الحد المشترك: من ارتفاع الشمس إلى الزوال. التفاصيل المذهبية تحتاج توثيق مصادر قبل التفعيل."
570
- },
571
- {
572
- "id": "suhoor",
573
- "category": "recommended_fasting_meal",
574
- "status": "proposed_not_consumed",
575
- "activation": {
576
- "owner": "islamic_events_registry.json",
577
- "note_ar": "أيام الصيام فقط."
 
578
  },
579
- "common": {
580
- "preferred": "آخر الليل",
581
- "end": "true_dawn"
 
 
 
 
 
 
 
 
 
582
  },
583
- "sources": [
584
- "quran_2_187"
585
- ],
586
- "research_status": "needs_sources",
587
- "note": "الأكل والشرب مباحان حتى يتبين الفجر الصادق (البقرة 187). تُوثَّق سنية السحور من مصدر حديثي قبل التفعيل."
588
- },
589
- {
590
- "id": "iftar",
591
- "category": "recommended_fasting_meal",
592
- "status": "proposed_not_consumed",
593
- "activation": {
594
- "owner": "islamic_events_registry.json",
595
- "note_ar": "أيام الصيام فقط."
596
  },
597
- "common": {
598
- "start": "sunset"
 
 
 
 
 
 
 
 
 
 
599
  },
600
- "research_status": "needs_sources",
601
- "note": "يدخل وقت الإفطار بغروب الشمس. حد الغروب الشمسي المحسوب مستقل عن تأخير موعد المغرب في طريقة الأردن."
602
- },
603
- {
604
- "id": "eclipse_prayer",
605
- "category": "occasional_prayer",
606
- "status": "proposed_not_consumed",
607
- "time_source": {
608
- "owner": "astronomy_events_registry.json",
609
- "practice_id": "eclipse_prayer",
610
- "note_ar": "وقتها من محرك الكسوف والخسوف المحلي (الجزء المرئي فقط) ولا يُحسب من هذا الملف."
611
  },
612
- "restriction_relation": {
613
- "status": "pending_scholar_review",
614
- "note_ar": "علاقتها بأوقات النهي مذهبية؛ لا تُدرج قيمة قبل توثيق المصادر."
615
  }
 
616
  }
617
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  }
 
1
  {
2
+ "schema_name": "islamic_clock_worship_fiqh_registry",
3
+ "schema_version": "1.3.0",
4
+ "registry_id": "worship_fiqh",
5
+ "project": "ساعة المسلم / الخط الزمني الروحي",
6
+ "language": "ar",
7
+ "direction": "rtl",
8
+ "status": "researched_v2_madhhab_restriction_windows",
9
+ "scope_ar": "أحكام العبادات المرتبطة بالزمن فقط مع الخلاف بين المذاهب الأربعة. لا يملك الحساب الفلكي أو الألوان أو الأيقونات أو منبهات المستخدم.",
10
+ "architecture_contract": {
11
+ "time_geometry_owner": "fiqh_time_registry.json",
12
+ "worship_event_owner": "islamic_events_registry.json",
13
+ "visual_owner": "visual_states_registry.json",
14
+ "user_alarm_owner": "user_time_markers_registry.json",
15
+ "knowledge_owner": "contextual_knowledge_registry.json",
16
+ "no_fixed_solar_minutes": true
17
+ },
18
+ "madhhabs": {
19
+ "hanafi": "الحنفي",
20
+ "maliki": "المالكي",
21
+ "shafii": "الشافعي",
22
+ "hanbali": "الحنبلي"
23
+ },
24
+ "rules": [
25
+ {
26
+ "id": "fajr",
27
+ "category": "obligatory_prayer",
28
+ "common": {
29
+ "start": "true_dawn",
30
+ "end": "sunrise"
31
+ },
32
+ "note": "أصل النافذة مشترك؛ درجات الفضيلة والاختيار تفصيل مذهبي."
33
+ },
34
+ {
35
+ "id": "dhuhr",
36
+ "category": "obligatory_prayer",
37
+ "common": {
38
+ "start": "zawal"
39
+ },
40
+ "madhhab": {
41
+ "hanafi": {
42
+ "end": "asr_shadow_2",
43
+ "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: asr_shadow_1"
44
+ },
45
+ "maliki": {
46
+ "end": "asr_shadow_1"
47
+ },
48
+ "shafii": {
49
+ "end": "asr_shadow_1"
50
+ },
51
+ "hanbali": {
52
+ "end": "asr_shadow_1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
+ },
55
+ "sources": [
56
+ "hanafi_asr",
57
+ "asr_comparison"
58
+ ]
59
+ },
60
+ {
61
+ "id": "asr",
62
+ "category": "obligatory_prayer",
63
+ "common": {
64
+ "end": "sunset"
65
+ },
66
+ "madhhab": {
67
+ "hanafi": {
68
+ "start": "asr_shadow_2",
69
+ "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: asr_shadow_1",
70
+ "late": "يكره التأخير إلى تغير/اصفرار الشمس بلا عذر"
71
+ },
72
+ "maliki": {
73
+ "start": "asr_shadow_1",
74
+ "note": "يفصل بين الاختياري والضروري"
75
+ },
76
+ "shafii": {
77
+ "start": "asr_shadow_1",
78
+ "note": "توجد مراتب للوقت"
79
+ },
80
+ "hanbali": {
81
+ "start": "asr_shadow_1",
82
+ "note": "وقت اختيار ثم ضرورة إلى الغروب"
83
  }
84
+ },
85
+ "sources": [
86
+ "hanafi_asr",
87
+ "asr_comparison"
88
+ ]
89
+ },
90
+ {
91
+ "id": "maghrib",
92
+ "category": "obligatory_prayer",
93
+ "common": {
94
+ "start": "sunset"
95
+ },
96
+ "madhhab": {
97
+ "hanafi": {
98
+ "end": "isha_entry_hanafi"
99
+ },
100
+ "maliki": {
101
+ "end": "red_twilight_end"
102
+ },
103
+ "shafii": {
104
+ "end": "red_twilight_end",
105
+ "note": "يوجد تفصيل قديم/جديد في المذهب؛ تحفظ الروايات ولا تختزل"
106
+ },
107
+ "hanbali": {
108
+ "end": "red_twilight_end"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
+ },
111
+ "sources": [
112
+ "maghrib_shafii",
113
+ "isha_twilight"
114
+ ]
115
  },
116
+ {
117
+ "id": "isha_entry",
118
+ "category": "obligatory_prayer",
119
+ "madhhab": {
120
+ "hanafi": {
121
+ "start": "white_twilight_end",
122
+ "variant": "أبو يوسف ومحمد ورواية عن أبي حنيفة: red_twilight_end"
123
+ },
124
+ "maliki": {
125
+ "start": "red_twilight_end"
126
+ },
127
+ "shafii": {
128
+ "start": "red_twilight_end"
129
+ },
130
+ "hanbali": {
131
+ "start": "red_twilight_end"
132
+ }
133
+ },
134
+ "sources": [
135
+ "isha_twilight"
136
+ ]
137
+ },
138
+ {
139
+ "id": "isha_periods",
140
+ "category": "obligatory_prayer",
141
+ "madhhab": {
142
+ "hanafi": {
143
+ "outer_end": "next_true_dawn",
144
+ "note": "داخل المذهب مراتب فضيلة قبل ذلك"
145
+ },
146
+ "maliki": {
147
+ "preferred_end": "first_third_of_night",
148
+ "outer_end": "next_true_dawn",
149
+ "note": "تفصيل الاختياري/الضروري يحفظ في profile المذهب"
150
+ },
151
+ "shafii": {
152
+ "preferred_end": "first_third_of_night",
153
+ "outer_end": "next_true_dawn"
154
+ },
155
+ "hanbali": {
156
+ "preferred_end": "first_third_of_night",
157
+ "allowed_to": "islamic_midnight",
158
+ "necessity_end": "next_true_dawn"
159
+ }
160
+ },
161
+ "sources": [
162
+ "isha_end",
163
+ "isha_end_detail"
164
+ ]
165
+ },
166
+ {
167
+ "id": "duha",
168
+ "category": "voluntary_prayer",
169
+ "common": {
170
+ "start": "sunrise_clear",
171
+ "end": "solar_noon",
172
+ "ruling": "recommended",
173
+ "best_time": "when_sun_is_high_and_heat_has_increased"
174
+ },
175
+ "madhhab": {
176
+ "hanafi": "recommended",
177
+ "maliki": "recommended",
178
+ "shafii": "emphasized_sunnah",
179
+ "hanbali": "recommended"
180
+ },
181
+ "implementation": "لا +15 دقيقة بعد الشروق ولا -5 قبل الزوال؛ الحدود من الهندسة الشمسية.",
182
+ "sources": [
183
+ "duha"
184
+ ],
185
+ "display_option_note": "يستطيع المحرك إنهاء الضحى قبيل الزوال بإضافة display_end_estimate_id = zawal_pre_noon لمن يقول بالنهي وقت الاستواء (الحنفية والشافعية والحنابلة)، ولا يناسب المالكية. يُترك لقرار شرعي."
186
+ },
187
+ {
188
+ "id": "witr",
189
+ "category": "night_prayer",
190
+ "common": {
191
+ "start_relation": "after_isha",
192
+ "end": "next_true_dawn",
193
+ "preferred": "آخر الليل لمن يثق بالاستيقاظ",
194
+ "early_option": "قبل النوم لمن يخشى ألا يستيقظ"
195
+ },
196
+ "sources": [
197
+ "witr"
198
+ ]
199
+ },
200
+ {
201
+ "id": "qiyam",
202
+ "category": "night_prayer",
203
+ "common": {
204
+ "start_relation": "after_isha",
205
+ "end": "next_true_dawn",
206
+ "preferred_window": "last_third_start_to_next_true_dawn"
207
+ },
208
+ "sources": [
209
+ "witr",
210
+ "last_third"
211
+ ]
212
+ },
213
+ {
214
+ "id": "jumuah",
215
+ "category": "obligatory_prayer",
216
+ "madhhab": {
217
+ "hanafi": {
218
+ "start": "zawal",
219
+ "end_relation": "dhuhr_end",
220
+ "end": "asr"
221
+ },
222
+ "maliki": {
223
+ "start": "zawal",
224
+ "end_relation": "dhuhr_end_with_detail",
225
+ "end": "asr"
226
+ },
227
+ "shafii": {
228
+ "start": "zawal",
229
+ "end_relation": "dhuhr_end",
230
+ "end": "asr"
231
+ },
232
+ "hanbali": {
233
+ "start_variant": "before_zawal_allowed",
234
+ "preferred_display": "after_zawal",
235
+ "end_relation": "asr_entry",
236
+ "start": "zawal",
237
+ "end": "asr"
238
+ }
239
+ },
240
+ "sources": [
241
+ "jumuah"
242
+ ],
243
+ "calendar_rule": {
244
+ "type": "weekly",
245
+ "weekday": "friday"
246
+ },
247
+ "implementation_note": "end_relation الأصلية تبقى للتوثيق؛ end صريح لأن asr يُحل حسب المذهب (rule_start). عند الحنابلة start=zawal هو العرض المفضَّل، ويبقى start_variant للتجويز قبل الزوال."
248
+ },
249
+ {
250
+ "id": "after_fajr_nafl_restriction",
251
+ "category": "prayer_restriction",
252
+ "scope": "general_voluntary_prayer",
253
+ "display_policy": "show_without_worship_tracking",
254
+ "madhhab": {
255
+ "hanafi": {
256
+ "activation": "boundary",
257
+ "start": "true_dawn",
258
+ "end": "sunrise",
259
+ "exception": "fajr_sunnah_before_obligatory_prayer"
260
+ },
261
+ "maliki": {
262
+ "activation": "boundary",
263
+ "start": "true_dawn",
264
+ "end": "sunrise",
265
+ "exception": "fajr_sunnah_before_obligatory_prayer"
266
+ },
267
+ "shafii": {
268
+ "activation": "after_prayer",
269
+ "requires": "fajr_prayer_performed",
270
+ "display_start": "fajr",
271
+ "end": "sunrise"
272
+ },
273
+ "hanbali": {
274
+ "activation": "boundary",
275
+ "start": "true_dawn",
276
+ "end": "sunrise",
277
+ "exception": "fajr_sunnah_before_obligatory_prayer",
278
+ "variant": "رواية: يتعلق النهي بأداء صلاة الفجر"
279
+ }
280
+ },
281
+ "critical": "يختلف بدء النهي: عند الحنفية والمالكية والمشهور عند الحنابلة من طلوع الفجر، وعند الشافعية بعد أداء صلاة الفجر. العرض البرتقالي لا يسجل أداء العبادة.",
282
+ "note": "تنتهي هذه الطبقة عند الشروق، ثم تبدأ طبقة النهي الشمسي الأحمر إلى ارتفاع الشمس. الاستثناءات المتعلقة بنوع الصلاة تعرض في تفاصيل الحكم ولا تغيّر الحد الزمني العام بلا دليل خاص.",
283
+ "sources": [
284
+ "post_prayer_prohibition",
285
+ "prohibition_comparative",
286
+ "prohibition_trigger_comparative"
287
+ ]
288
+ },
289
+ {
290
+ "id": "after_asr_nafl_restriction",
291
+ "category": "prayer_restriction",
292
+ "scope": "general_voluntary_prayer",
293
+ "display_policy": "conditional_window_without_worship_tracking",
294
+ "common": {
295
+ "activation": "after_prayer",
296
+ "requires": "asr_prayer_performed",
297
+ "display_start": "asr",
298
+ "end": "sunset_approach"
299
+ },
300
+ "madhhab": {
301
+ "hanafi": {
302
+ "activation": "after_prayer",
303
+ "requires": "asr_prayer_performed"
304
+ },
305
+ "maliki": {
306
+ "activation": "after_prayer",
307
+ "requires": "asr_prayer_performed"
308
+ },
309
+ "shafii": {
310
+ "activation": "after_prayer",
311
+ "requires": "asr_prayer_performed"
312
+ },
313
+ "hanbali": {
314
+ "activation": "after_prayer",
315
+ "requires": "asr_prayer_performed"
316
+ }
317
+ },
318
+ "critical": "لا يبدأ الحكم بمجرد دخول وقت العصر، بل بعد أداء صلاة العصر. ولأن التطبيق لا يتتبع أداء الصلاة، تعرض المسطرة نافذة برتقالية مشروطة من دخول العصر، لا حكمًا محققًا على المستخدم.",
319
+ "note": "تنتهي الطبقة المشروطة عند اقتراب الغروب، ثم تظهر طبقة النهي الشمسي الأحمر حتى الغروب. الاستثناءات وذوات الأسباب تختلف بين المذاهب وتبقى في تفاصيل الحكم.",
320
+ "sources": [
321
+ "post_prayer_prohibition",
322
+ "prohibition_comparative"
323
+ ]
324
+ },
325
+ {
326
+ "id": "absolute_solar_restrictions",
327
+ "category": "solar_prayer_restriction",
328
+ "scope": "absolute_solar_windows",
329
+ "madhhab": {
330
+ "hanafi": {
331
+ "windows": [
332
+ {
333
+ "start": "sunrise",
334
+ "end": "sunrise_clear"
335
  },
336
+ {
337
+ "start": "solar_noon",
338
+ "end": "zawal",
339
+ "display_estimate_id": "zawal_pre_noon"
 
 
 
 
 
 
 
 
 
340
  },
341
+ {
342
+ "start": "sunset_approach",
343
+ "end": "sunset"
344
+ }
345
+ ],
346
+ "variant": "رُوي عن أبي يوسف الترخيص في الاستواء يوم الجمعة، وليس هو العرض الافتراضي للمذهب هنا."
347
+ },
348
+ "maliki": {
349
+ "windows": [
350
+ {
351
+ "start": "sunrise",
352
+ "end": "sunrise_clear"
353
  },
354
+ {
355
+ "start": "sunset_approach",
356
+ "end": "sunset"
357
+ }
358
+ ],
359
+ "note": "لا تعرض نافذة الاستواء إلى الزوال في الاختيار المالكي."
360
+ },
361
+ "shafii": {
362
+ "windows": [
363
+ {
364
+ "start": "sunrise",
365
+ "end": "sunrise_clear"
366
  },
367
+ {
368
+ "start": "solar_noon",
369
+ "end": "zawal",
370
+ "exclude_weekdays": [
371
+ "friday"
372
+ ],
373
+ "display_estimate_id": "zawal_pre_noon"
 
 
 
 
 
 
374
  },
375
+ {
376
+ "start": "sunset_approach",
377
+ "end": "sunset"
378
+ }
379
+ ],
380
+ "note": "يستثنى الاستواء يوم الجمعة من العرض وفق المعتمد المسجل في المصدر المقارن."
381
+ },
382
+ "hanbali": {
383
+ "windows": [
384
+ {
385
+ "start": "sunrise",
386
+ "end": "sunrise_clear"
387
  },
388
+ {
389
+ "start": "solar_noon",
390
+ "end": "zawal",
391
+ "display_estimate_id": "zawal_pre_noon"
 
 
 
 
 
 
 
392
  },
393
+ {
394
+ "start": "sunset_approach",
395
+ "end": "sunset"
396
  }
397
+ ]
398
  }
399
+ },
400
+ "implementation": "الحدود هندسة شمسية بلا دقائق ثابتة؛ نافذة النهي قبيل الزوال (zawal_pre_noon) سياسة عرض تقديرية تُقرأ من display_estimates وليست بديلًا عن الحد الفلكي.",
401
+ "note": "اختيار النوافذ نفسها مذهبي. تعرض نافذة الزوال قبل الزوال فقط (10 دقائق افتراضيًا، والخيار 15) لأن ما بعد الزوال وقت جواز الصلاة، مع بيان أن الأبحاث المذكورة تقدر المنطقة الأقرب للزوال عادةً بنحو 5–10 دقائق. أما استثناءات أنواع الصلوات كذوات الأسباب والقضاء والجنازة والطواف فلا تختزل في لون زمني عام، وتعرض في تفاصيل الحكم.",
402
+ "sources": [
403
+ "prohibition_comparative",
404
+ "solar_prohibition",
405
+ "zawal_duration_research"
406
+ ]
407
+ },
408
+ {
409
+ "id": "eid_fitr_prayer",
410
+ "category": "occasional_prayer",
411
+ "calendar_rule": {
412
+ "calendar": "hijri",
413
+ "month": 10,
414
+ "day": 1,
415
+ "requires_month_confirmation": true
416
+ },
417
+ "common": {
418
+ "start": "sunrise_clear",
419
+ "end": "solar_noon"
420
+ },
421
+ "note": "من ارتفاع الشمس قدر رمح إلى قبيل الزوال. عند الشافعية يدخل وقتها من طلوع الشمس والأفضل بعد ارتفاعها. الحد الأعلى هو الزوال، وsolar_noon يقابله هنا بلا هامش.",
422
+ "research_status": "added_pending_scholar_review",
423
+ "sources": []
424
+ },
425
+ {
426
+ "id": "eid_adha_prayer",
427
+ "category": "occasional_prayer",
428
+ "calendar_rule": {
429
+ "calendar": "hijri",
430
+ "month": 12,
431
+ "day": 10,
432
+ "requires_month_confirmation": true
433
+ },
434
+ "common": {
435
+ "start": "sunrise_clear",
436
+ "end": "solar_noon"
437
+ },
438
+ "note": "يُستحب تعجيلها في الأضحى. نفس حدود صلاة عيد الفطر مع بقاء الاختلاف في الأفضلية.",
439
+ "research_status": "added_pending_scholar_review",
440
+ "sources": []
441
+ },
442
+ {
443
+ "id": "friday_response_hour",
444
+ "category": "recommended_time",
445
+ "calendar_rule": {
446
+ "type": "weekly",
447
+ "weekday": "friday"
448
+ },
449
+ "display_policy": "information_only_without_worship_tracking",
450
+ "common": {
451
+ "start": "last_daylight_hour_start",
452
+ "end": "sunset"
453
+ },
454
+ "note": "يُعرض آخر جزء من نهار الجمعة (اثنا عشر جزءًا للنهار من الشروق إلى الغروب). ويرى كثير من العلماء أنها تمتد من العصر إلى الغروب، وقيل: من جلوس الإمام إلى انقضاء الصلاة. الخلاف يبقى في تفاصيل الحكم، والنافذة معلومة لا ادعاء.",
455
+ "research_status": "added_pending_scholar_review",
456
+ "sources": []
457
+ },
458
+ {
459
+ "id": "ramadan_fast",
460
+ "category": "obligatory_fasting",
461
+ "common": {
462
+ "start": "true_dawn",
463
+ "end": "sunset"
464
+ },
465
+ "note": "ثبوت بداية الشهر من سلطة التقويم/الرؤية المعتمدة، لا من هذا الملف.",
466
+ "sources": [
467
+ "quran_2_187"
468
+ ]
469
+ },
470
+ {
471
+ "id": "monday_thursday_fast",
472
+ "category": "recommended_fasting",
473
+ "common": {
474
+ "start": "true_dawn",
475
+ "end": "sunset"
476
+ },
477
+ "note": "اختيار اليوم وجدولته من islamic_events_registry.json."
478
+ }
479
+ ],
480
+ "required_time_engine_outputs": [
481
+ {
482
+ "id": "asr_shadow_1",
483
+ "type": "shadow_factor",
484
+ "factor": 1,
485
+ "exclude_noon_shadow": true
486
+ },
487
+ {
488
+ "id": "asr_shadow_2",
489
+ "type": "shadow_factor",
490
+ "factor": 2,
491
+ "exclude_noon_shadow": true
492
+ },
493
+ {
494
+ "id": "first_third_of_night",
495
+ "type": "derived_night_fraction",
496
+ "fraction": "1/3",
497
+ "from": "sunset",
498
+ "to": "next_true_dawn"
499
+ },
500
+ {
501
+ "id": "red_twilight_end",
502
+ "type": "solar_altitude"
503
+ },
504
+ {
505
+ "id": "white_twilight_end",
506
+ "type": "solar_altitude"
507
+ },
508
+ {
509
+ "id": "islamic_midnight",
510
+ "type": "derived_night_fraction",
511
+ "fraction": "1/2",
512
+ "from": "sunset",
513
+ "to": "next_true_dawn"
514
+ },
515
+ {
516
+ "id": "last_third_start",
517
+ "type": "derived_night_fraction",
518
+ "fraction": "2/3",
519
+ "from": "sunset",
520
+ "to": "next_true_dawn"
521
+ },
522
+ {
523
+ "id": "sunrise_clear",
524
+ "type": "solar_altitude"
525
+ },
526
+ {
527
+ "id": "sunset_approach",
528
+ "type": "solar_altitude"
529
+ },
530
+ {
531
+ "id": "solar_noon",
532
+ "type": "solar_transit"
533
+ },
534
+ {
535
+ "id": "zawal",
536
+ "type": "solar_transition"
537
+ },
538
+ {
539
+ "id": "last_daylight_hour_start",
540
+ "type": "derived_day_fraction",
541
+ "fraction": "11/12",
542
+ "from": "sunrise",
543
+ "to": "sunset"
544
+ }
545
+ ],
546
+ "sources": {
547
+ "hanafi_asr": {
548
+ "title": "نقول حنفية في المثل والمثلين",
549
+ "url": "https://islamqa.org/hanafi/darul-iftaa-chicago/271053/"
550
+ },
551
+ "asr_comparison": {
552
+ "title": "الفرق بين الحنفية والجمهور في أول العصر",
553
+ "url": "https://islamqa.info/ar/answers/220820"
554
+ },
555
+ "maghrib_shafii": {
556
+ "title": "دائرة الإفتاء الأردنية — وقت المغرب",
557
+ "url": "https://aliftaa.jo/fatwa/3399/CatQuestion.aspx?Id=133"
558
+ },
559
+ "isha_twilight": {
560
+ "title": "الموسوعة الفقهية — الشفق ووقت العشاء",
561
+ "url": "https://dorar.net/feqhia/846/"
562
+ },
563
+ "isha_end": {
564
+ "title": "وقت خروج العشاء في المذاهب الأربعة",
565
+ "url": "https://www.islamweb.org/ar/fatwa/71957/"
566
+ },
567
+ "isha_end_detail": {
568
+ "title": "تفصيل آخر وقت العشاء",
569
+ "url": "https://www.alqayim.org/ar/fatwa/1971"
570
+ },
571
+ "duha": {
572
+ "title": "الموسوعة الفقهية — صلاة الضحى",
573
+ "url": "https://dorar.net/feqhia/1261/"
574
+ },
575
+ "witr": {
576
+ "title": "الموسوعة الفقهية — أفضل وقت للوتر",
577
+ "url": "https://dorar.net/feqhia/1241/"
578
+ },
579
+ "last_third": {
580
+ "title": "صحيح البخاري 1145",
581
+ "url": "https://sunnah.com/bukhari:1145"
582
+ },
583
+ "jumuah": {
584
+ "title": "مذاهب العلماء في وقت الجمعة",
585
+ "url": "https://islamweb.net/ar/fatwa/23917/"
586
+ },
587
+ "post_prayer_prohibition": {
588
+ "title": "صحيح مسلم 827",
589
+ "url": "https://sunnah.com/muslim:827"
590
+ },
591
+ "prohibition_comparative": {
592
+ "title": "بحث مقارن في أوقات النهي",
593
+ "url": "https://islamport.com/l/msl/3529/9.htm"
594
+ },
595
+ "prohibition_trigger_comparative": {
596
+ "title": "بحث مقارن في تعلق نهي الفجر بطلوعه أو بأداء الصلاة",
597
+ "url": "https://islamport.com/l/msl/3529/17.htm"
598
+ },
599
+ "solar_prohibition": {
600
+ "title": "صحيح مسلم 831",
601
+ "url": "https://sunnah.com/muslim:831"
602
+ },
603
+ "zawal_duration_research": {
604
+ "title": "تقدير المدة القريبة من الزوال وبيان أنها غير ثابتة بعشرين دقيقة",
605
+ "url": "https://saadalkhathlan.com/scientific-series/lessons/32181"
606
+ },
607
+ "quran_2_187": {
608
+ "title": "البقرة 187",
609
+ "reference": "Quran 2:187"
610
+ }
611
+ },
612
+ "validation": {
613
+ "worship_only": true,
614
+ "no_colors": true,
615
+ "no_emoji": true,
616
+ "no_user_alarms": true,
617
+ "no_completion_tracking": true,
618
+ "no_fixed_solar_minute_offsets": true,
619
+ "preserve_madhhab_variants": true,
620
+ "calendar_rules_supported": true,
621
+ "categories_requiring_visual_state": [
622
+ "occasional_prayer",
623
+ "recommended_time"
624
+ ],
625
+ "visual_note": "الفئتان occasional_prayer وrecommended_time تحتاجان حالة في visual_states_registry.category_states وإلا تُتجاوز القاعدتان بصمت."
626
+ },
627
+ "research_note": "هذه طبقة بيانات للتطبيق وليست فتوى شخصية. حيث لم تكف المصادر لإغلاق تفصيل مذهبي دقيق، أبقي التفصيل معلّقًا بدل اختراع قيمة.",
628
+ "changelog": [
629
+ "1.3.0: إصلاح jumuah: لم يكن ينتج أي نافذة (نهاية غير محلولة، وبداية الحنبلي مفقودة)؛ أُضيفت start/end وقاعدة تقويم weekly:friday",
630
+ "1.3.0: نافذة الزوال: من مركزة ±10 دقائق حول الاستواء إلى قبيل الزوال فقط (10 دقائق، والخيار 15)، لأن ما بعد الزوال وقت جواز الصلاة",
631
+ "1.3.0: انتقال التقديرات إلى display_estimates العليا ليصل إليها كل من start وend (كانت duha لا ترى إلا العليا)",
632
+ "1.3.0: إضافة صلاة العيدين وساعة الإجابة بعد العصر يوم الجمعة (قيد المراجعة الشرعية)",
633
+ "1.3.0: اكتمال required_time_engine_outputs"
634
+ ],
635
+ "display_estimates": {
636
+ "note_ar": "تقديرات عرض احتياطية تُقرأ من محرك الفترات. before_minutes قبل الحد وafter_minutes بعده. allowed_before_minutes هي القيم التي يقبلها خيار المستخدم.",
637
+ "zawal_pre_noon": {
638
+ "boundary": "solar_noon",
639
+ "before_minutes": 10,
640
+ "after_minutes": 0,
641
+ "allowed_before_minutes": [
642
+ 10,
643
+ 15
644
+ ],
645
+ "label_ar": "نافذة عرض النهي قبيل الزوال",
646
+ "note_ar": "النهي عن الصلاة وقت قيام الظهيرة حتى تزول الشمس، فالنافذة قبل الزوال فقط، وما بعده وقت الظهر. المدة تقديرية للعرض: الأبحاث المذكورة تقدّر المنطقة القريبة من الزوال بنحو 5–10 دقائق، وخيار 15 للتحوّط. الحد الفلكي المحسوب يبقى محفوظًا مستقلًا.",
647
+ "source_id": "zawal_duration_research",
648
+ "replaces": "zawal_20_minutes"
649
+ }
650
+ }
651
  }