diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cf91e6..e0a61a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,9 +65,27 @@ jobs: - name: (د) حارس سلامة البيان (بلا شبكة) run: python scripts/check_sync.py --validate + # (AR) على `workflow_dispatch` يكون `base_ref` فارغًا ⇒ `origin/` ⇒ يفشل + # `git show` ⇒ قفلُ أساسٍ فارغ ⇒ يتخطّى الحارسُ نفسَه **صامتًا** ويُقرأ + # أخضرَ. الأخضرُ حينها يعني «لم يُقَس» لا «مرّ»؛ فنقصره على الـPR صراحةً. - name: (أ) حارس القفل — لا تثبيت بصمةٍ دون مراجعة فصلها + if: github.event_name == 'pull_request' run: | BASE="origin/${{ github.base_ref }}" - git show "$BASE:sync/sources.lock.json" > base.lock.json 2>/dev/null || : > base.lock.json + # (AR) فرّق بين «لا قفلَ في الأساس» (تخطٍّ مشروع) و«تعذّر بلوغُ الأساس» + # (حمرة). كان `|| :` يبتلع الحالتين معًا، فأيُّ فشلٍ لـ`git show` — + # مرجعٌ غيرُ مجلوب، نقلُ مسارِ القفل — يُنتج أساسًا فارغًا فيتخطّى + # الحارسُ نفسَه ويُرجع صفرًا: فاشلٌ مفتوحًا. + git rev-parse --verify "$BASE" >/dev/null 2>&1 \ + || { echo "::error::مرجعُ الأساس $BASE غيرُ مجلوب — الحارسُ لم يُقَس."; exit 1; } + if git cat-file -e "$BASE:sync/sources.lock.json" 2>/dev/null; then + git show "$BASE:sync/sources.lock.json" > base.lock.json + else + : > base.lock.json # لا قفلَ في الأساس فعلًا — تخطٍّ مشروع + fi + # (AR) بيانُ الأساس يلزم لقياس المفاتيح **المُسقَطة**: المفتاحُ المحذوف + # لا أثرَ له في البيان الحاليّ، فلا يُعرَف أيُّ فصلٍ كان يستشهد به. + git show "$BASE:sync/sources.yaml" > base.sources.yaml 2>/dev/null || : > base.sources.yaml git diff --name-only "$BASE"...HEAD > changed.txt - python scripts/check_sync.py --guard-lock --base base.lock.json --changed-files changed.txt + python scripts/check_sync.py --guard-lock --base base.lock.json \ + --base-manifest base.sources.yaml --changed-files changed.txt diff --git a/.github/workflows/sync-check.yml b/.github/workflows/sync-check.yml index 78285e4..022693b 100644 --- a/.github/workflows/sync-check.yml +++ b/.github/workflows/sync-check.yml @@ -56,9 +56,21 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + set +e set +e python scripts/check_sync.py --ref "${{ steps.ref.outputs.ref }}" --json > report.json - echo "drift=$?" >> "$GITHUB_OUTPUT" + RC=$? + set -e + # (AR) 0 و1 وحدهما قياسان (لا انجراف / انجراف). أيُّ رمزٍ سواهما — و`2` + # يُرجَع عند غياب القفل قبل طباعةِ أيّ JSON — كان يجعل الشرطين + # كاذبَين معًا: لا قضيّةَ تُفتح، ولا خطوةَ «لا انجراف»، والوظيفةُ + # خضراء وهي لم تقس شيئًا. + if [ "$RC" != "0" ] && [ "$RC" != "1" ]; then + echo "::error::رمزُ خروجٍ غيرُ مقيس ($RC) — لم يُقَس الانجراف." + cat report.json || true + exit "$RC" + fi + echo "drift=$RC" >> "$GITHUB_OUTPUT" cat report.json - name: صياغة جسم القضيّة diff --git a/scripts/check_sync.py b/scripts/check_sync.py index 5f946cc..4e5dcee 100644 --- a/scripts/check_sync.py +++ b/scripts/check_sync.py @@ -8,6 +8,14 @@ - ملفّ/مجلد كامل: بصمة git (blob/tree sha) عبر `gh api .../contents`. - نطاق أسطر (تقليل الضجيج): نجلب المحتوى الخام، نقتطع النطاق، ونبصمه sha256. هكذا لا يُطلِق تعديلٌ خارج النطاق إنذارًا كاذبًا، ويُرصد «تعفّن» المنطقة الموثَّقة. + - أسماء المدخلات `{path, names: true}`: نبصم **أسماءَ وأنواعَ** مدخلات المجلّد + (مرتَّبةً) لا محتواها. هذا النمطُ هو الوحيد الذي يطابق دعوى «هذا المجلّد يحوي + كذا وكذا» — فبصمةُ الشجرة (tree sha) تنقلب مع أيّ تعديلِ محتوًى داخله فتُنتج + إنذارًا أسبوعيًّا دائمًا، وهو عينُ الضجيجِ الذي يُسكِت الكاشفَ كلَّه. + ويُكتب `path: "."` لبصم جذر المستودع (دعاوى خريطة الشجرة). + - مصدرٌ داخليّ `self:`: يُبصَم من **هذا المستودع** لا من مستودع اللغة، بلا + شبكة. لأنّ فصولًا (مثل freshness.md) توثّق أدواتِ الدليل نفسِها، فكانت تتعفّن + عنها بلا كاشف — وقد وقع ذلك فعلًا: وصف الفصلُ سلوكًا نُقض في نفس اليوم. الأطوار: (افتراضيّ) فحص وتقرير؛ يفشل (1) عند الانجراف. @@ -59,13 +67,28 @@ # ── تطبيع المصادر ────────────────────────────────────────────────────────────── def normalize_source(s) -> dict: - """يحوّل مصدرًا (نصّ أو كائن {path, lines}) إلى صيغة موحّدة بمفتاح فريد.""" + """يحوّل مصدرًا (نصّ أو كائن {path, lines|names}) إلى صيغة موحّدة بمفتاح فريد.""" if isinstance(s, str): - return {"path": s, "lines": None, "key": s} + return {"path": s, "lines": None, "names": False, "key": s} path = s["path"] lines = s.get("lines") - key = f"{path}#L{lines}" if lines else path - return {"path": path, "lines": lines, "key": key} + names = bool(s.get("names")) + if names: + key = f"{path}#names" + elif lines: + key = f"{path}#L{lines}" + else: + key = path + return {"path": path, "lines": lines, "names": names, "key": key} + + +def is_self(spec: dict) -> bool: + """مصدرٌ داخليّ: يُبصَم من مستودع الدليل نفسِه، لا من مستودع اللغة.""" + return spec["path"].startswith("self:") + + +def self_path(spec: dict) -> Path: + return ROOT / spec["path"][len("self:"):] def parse_lines(spec: str) -> tuple[int, int]: @@ -74,6 +97,16 @@ def parse_lines(spec: str) -> tuple[int, int]: # ── جلب البصمات عبر gh ───────────────────────────────────────────────────────── +def _run(args: list[str]) -> str | None: + """أمرٌ محلّيّ (git) — يعيد None عند أيّ فشل، فلا يُقرأ العجزُ نجاحًا.""" + try: + out = subprocess.run(args, capture_output=True, text=True, + check=True, encoding="utf-8") + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return None + return out.stdout + + def _gh(args: list[str]) -> str | None: try: out = subprocess.run(["gh", *args], capture_output=True, text=True, @@ -104,22 +137,99 @@ def _gh_tree_sha(repo: str, ref: str, path: str) -> str | None: return out.strip() if out else None +def gh_entry_names(repo: str, ref: str, path: str) -> str | None: + """أسماءُ وأنواعُ مدخلات المجلّد مرتَّبةً — لا محتواها.""" + api_path = "" if path in (".", "") else path + # (AR) `contents` يقتطع عند 1000 مدخلًا **صامتًا** — لا يُخطئ. فقائمةٌ مقتطعةٌ + # تُبصَم بصمةً ثابتةً تخضرّ أبدًا. نجعل التجاوزَ «متعذّرًا» لا بصمة. + out = _gh(["api", f"repos/{repo}/contents/{api_path}?ref={ref}", + "--jq", 'if type=="array" then (if length >= 1000 then "CAP" ' + 'else ([.[] | .type + ":" + .name] | sort | join("\n")) end) ' + 'else "NOTDIR" end']) + if out is None: + return None + val = out.strip() + if val == "CAP": + print(f"⚠️ {path}: تجاوز سقفَ 1000 مدخلٍ في contents API — لم يُقَس.", + file=sys.stderr) + return None + return None if val in ("", "NOTDIR") else val + + +def _names_digest(listing: str) -> str: + return "names:" + hashlib.sha256(listing.encode("utf-8")).hexdigest()[:16] + + def gh_raw(repo: str, ref: str, path: str) -> str | None: return _gh(["api", "-H", "Accept: application/vnd.github.raw", f"repos/{repo}/contents/{path}?ref={ref}"]) +def _chunk_digest(raw: str, lines_spec: str | None) -> str | None: + lines = raw.splitlines() + if lines_spec: + a, b = parse_lines(lines_spec) + lines = lines[a - 1:b] + # (AR) لا مقطعَ ⇒ «متعذّر» يحمرّ، لا بصمةَ خواءٍ تُثبَّت فتخضرّ للأبد. + # أربعُ حالاتٍ متباينةٍ كانت تُنتج البصمةَ نفسَها (`e3b0c442…`): ردٌّ فارغٌ + # من gh · نطاقٌ خارجَ الملفّ · ملفٌّ صفريّ · ملفٌّ انكمش دون النطاق. + # فلو انكمش الملفُّ لاحمرَّ الفحصُ مرّةً ثمّ ثبّته `--update`، ومنذُها + # يبقى المصدرُ أخضرَ وهو غيرُ مقيسٍ أصلًا. + if not lines: + return None + digest = hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()[:16] + return f"sha256:{digest}" + + +def fetch_self_sha(spec: dict) -> str | None: + """بصمةُ مصدرٍ داخليّ من قرص هذا المستودع — بلا شبكة.""" + p = self_path(spec) + if spec["names"]: + # (AR) الوعدُ في freshness.md أن نستبعدَ ما لا يخزّنه git. مسحُ القرص + # بقائمةِ استثناءاتٍ منثورةٍ لا يفي به: قِيس أنّ جذرَ مستودعِ اللغة + # 43 مدخلًا في git و70 على القرص (`book/` · `.venv` · بل ملفّاتُ + # خطوةِ الحارسِ نفسِها). فنشتقُّ الحقيقةَ من git لا من القرص. + if not p.is_dir(): + return None + # (AR) القرصُ يحوي ما لا يحويه git (`.git` · `__pycache__` · مخلّفاتُ بناء)، + # فبصمةٌ تشملها تنقلب بلا واقعةٍ في المستودع — ضجيجٌ غيرُ حتميّ. + # نستبعدها لتبقى البصمةُ دالّةً على الشجرة المُودَعة وحدها. + skip = {".git", "__pycache__", ".mypy_cache", ".pytest_cache"} + rel = spec["path"][len("self:"):].strip("/") + out = _run(["git", "-C", str(ROOT), "ls-tree", "HEAD", + (rel + "/") if rel else "./"]) + if out is not None: + listed = [] + for line in out.splitlines(): + if not line.strip(): + continue + meta, _, name = line.partition("\t") + kind = meta.split()[1] + listed.append(("dir:" if kind == "tree" else "file:") + + Path(name).name) + return _names_digest("\n".join(sorted(listed))) if listed else None + entries = sorted(("dir:" if c.is_dir() else "file:") + c.name + for c in p.iterdir() if c.name not in skip) + # (AR) مجلّدٌ فارغ ⇒ None كجانب github تمامًا (git لا يخزّن مجلّدًا فارغًا)، + # فلا نسجّل «بصمةَ خواء» تُقرأ نجاحًا. + return _names_digest("\n".join(entries)) if entries else None + if not p.is_file(): + return None + return _chunk_digest(p.read_text(encoding="utf-8"), spec["lines"]) + + def fetch_sha(repo: str, ref: str, spec: dict) -> str | None: - """بصمة المصدر: نطاق أسطر ⇒ sha256 للمقطع؛ غير ذلك ⇒ git sha.""" + """بصمة المصدر: أسماءُ مدخلات ⇒ names؛ نطاقُ أسطر ⇒ sha256؛ غيرُهما ⇒ git sha.""" + if is_self(spec): + return fetch_self_sha(spec) + if spec["names"]: + listing = gh_entry_names(repo, ref, spec["path"]) + return None if listing is None else _names_digest(listing) if spec["lines"]: raw = gh_raw(repo, ref, spec["path"]) if raw is None: return None - lines = raw.splitlines() - a, b = parse_lines(spec["lines"]) - chunk = "\n".join(lines[a - 1:b]) - digest = hashlib.sha256(chunk.encode("utf-8")).hexdigest()[:16] - return f"sha256:{digest}" + return _chunk_digest(raw, spec["lines"]) return gh_content_sha(repo, ref, spec["path"]) @@ -162,12 +272,24 @@ def collect_shas(manifest: dict, ref: str) -> tuple[dict, list]: # ── ملف القفل ────────────────────────────────────────────────────────────────── +def derived_version(repo: str, ref: str) -> str | None: + """`dev@<إيداع>` مشتقًّا من المرجعِ البعيد — لا منسوخًا بيدٍ فيتقادم.""" + out = _run(["git", "ls-remote", f"https://github.com/{repo}.git", ref]) + if not out or not out.split(): + return None + return f"{ref}@{out.split()[0][:8]}" + + def write_lock(manifest: dict, ref: str, shas: dict, version: str | None) -> None: + # (AR) الفصلُ يقول إنّ `covers_version` «يسمّي الإيداعَ الذي قِيست عنده + # البصمات». فلا يُنسخ من البيان — يُشتقّ. المكتوبُ بيدٍ صادقٌ اليومَ + # بالمصادفة، ولا شيءَ يمنع كذبَه غدًا (عددٌ منثورٌ نسخةً ثانيةً لواقعة). + auto = None if version is not None else derived_version(manifest["repo"], ref) lock = { "repo": manifest["repo"], "ref": ref, "covers_version": version if version is not None - else manifest.get("covers_version"), + else (auto or manifest.get("covers_version")), "sources": shas, } LOCKFILE.parent.mkdir(parents=True, exist_ok=True) @@ -177,20 +299,54 @@ def write_lock(manifest: dict, ref: str, shas: dict, version: str | None) -> Non # ── (د) حارس البيان دون شبكة ──────────────────────────────────────────────────── +# (AR) الأجزاءُ التي تستوجب تسجيلَ المصادر. كانت أربعةً (`frontend` · `backend` · +# `systems` · `sot`) فبقيت `architecture/` و`getting-started/` و`contributing/` +# **خارجَ المظلّة كلّيًّا** — وهي التي تحمل دعاوى أوامرَ يُشغّلها القارئ +# (`runner.py`، مسارُ البناء، شجرةُ المستودع)، فتعفّنت بلا كاشف. النطاقُ الآن +# كلُّ فصلٍ في قسمٍ من الأقسام السبعة؛ وصفحاتُ الجذر (`introduction` · +# `glossary` · `status` · `SUMMARY`) خارجه عمدًا: دعاواها عن الدليلِ نفسِه +# لا عن اللغة، ومنها ما يُسجَّل طوعًا حين يحمل دعوى مقيسة. +GUARDED_SECTIONS = ("frontend|backend|systems|sot|" + "architecture|getting-started|contributing") + + def technical_chapters_in_summary() -> set[str]: - """فصول SUMMARY التي تحت أقسامٍ تقنيّة تستوجب تسجيل مصادرها.""" + """فصول SUMMARY التي تحت أقسامٍ محروسة تستوجب تسجيل مصادرها.""" import re if not SUMMARY.exists(): return set() text = SUMMARY.read_text(encoding="utf-8") - # نلتقط مسارات الفصول داخل الأجزاء التقنيّة (الأماميّة/الخلفيّة/الأنظمة/مصدر الحقيقة) - techy = re.findall(r"\]\((src/(?:frontend|backend|systems|sot)/[\w\-/]+\.md)\)", + techy = re.findall(r"\]\((src/(?:" + GUARDED_SECTIONS + r")/[\w\-/]+\.md)\)", text) # SUMMARY يستخدم مسارات نسبيّة بلا بادئة src/ — لذا نعيد التقاطها بنمطين - rel = re.findall(r"\]\(((?:frontend|backend|systems|sot)/[\w\-/]+\.md)\)", text) + rel = re.findall(r"\]\(((?:" + GUARDED_SECTIONS + r")/[\w\-/]+\.md)\)", text) return {("src/" + p) for p in rel} | set(techy) +def self_drift_errors(manifest: dict) -> list[str]: + """تعفّنُ مصادرِ `self:` مقيسًا بلا شبكة — بوّابةُ PR لا كاشفٌ أسبوعيّ.""" + if not LOCKFILE.exists(): + # (AR) لا تُرجِع «سليم» لغيابِ ما تقيسُ به. حذفُ ملفٍّ واحدٍ كان يُطفئ + # البوّابةَ صامتةً وخضراء — وهو عينُ «الأخضرُ يعني لم يُقَس». + if any(is_self(s) for s in unique_specs(manifest).values()): + return ["لا يوجد sources.lock.json — فلا يمكن قياسُ تعفّنِ مصادر " + "`self:` المسجَّلة. شغّل `--update` أوّلًا."] + return [] + locked = json.loads(LOCKFILE.read_text(encoding="utf-8")).get("sources", {}) + out = [] + for key, spec in sorted(unique_specs(manifest).items()): + if not is_self(spec) or key not in locked: + continue + cur = fetch_self_sha(spec) + if cur is None or cur == locked[key]: + continue + chapters = ", ".join(chapters_for_key(manifest, key)) or "—" + out.append(f"مصدرٌ داخليّ تغيّر ولم تُثبَّت بصمتُه: {key}\n" + f" راجِع وعدِّل: {chapters}‏ ثمّ شغّل " + f"`python scripts/check_sync.py --update`.") + return out + + def cmd_validate(manifest: dict) -> int: errs, warns = [], [] seen: dict[str, str] = {} @@ -198,6 +354,17 @@ def cmd_validate(manifest: dict) -> int: if spec["key"] in seen and seen[spec["key"]] != ch: pass # نفس المصدر لفصول متعدّدة مسموح seen[spec["key"]] = ch + if spec["names"] and spec["lines"]: + errs.append(f"مصدرٌ يجمع `names` و`lines` معًا (متعارضان): {spec['key']}") + if is_self(spec): + p = self_path(spec) + if spec["names"] and not p.is_dir(): + errs.append(f"مصدرٌ داخليّ `names` ليس مجلّدًا: {spec['key']}") + elif not spec["names"] and not p.is_file(): + errs.append(f"مصدرٌ داخليّ غير موجود: {spec['key']}") + elif not spec["names"] and fetch_self_sha(spec) is None: + errs.append("مصدرٌ داخليّ نطاقُه خارجَ الملفّ (لا شيءَ يُقاس): " + f"{spec['key']}") if spec["lines"]: try: a, b = parse_lines(spec["lines"]) @@ -214,20 +381,35 @@ def cmd_validate(manifest: dict) -> int: if (ROOT / ch).exists() and ch not in chapter_files: warns.append(f"فصلٌ تقنيّ غير مسجَّل في sources.yaml: {ch}") + # (AR) مصادرُ `self:` لا تحتاج شبكة، فيمكن قياسُ تعفّنها **في نفس الـPR** لا أن + # ينتظر الأسبوعيّ. بدون هذا كان النمطُ الجديد كاشفًا أسبوعيًّا لا بوّابةَ PR: + # يُعدَّل `check_sync.py` وحده فيمرّ الـPR أخضرَ والفصلُ الذي يصفه متعفّن. + errs.extend(self_drift_errors(manifest)) + for w in warns: print(f"⚠️ {w}") for e in errs: print(f"❌ {e}") if errs: return 1 - print(f"✅ البيان سليم ({len(chapter_files)} فصلًا مسجَّلًا)" - + (f"، {len(warns)} تحذيرًا" if warns else "") + ".") - return 1 if warns else 0 + # (AR) لا تُنهِ سجلًّا فاشلًا بسطرِ «✅». مَن يمسح سجلَّ CI بعينه يقرأ آخرَ + # سطرٍ حكمًا؛ فكان «✅ البيان سليم … 1 تحذيرًا» يُقرأ نجاحًا ورمزُه 1. + if warns: + print(f"❌ البيانُ ناقص: {len(chapter_files)} فصلًا مسجَّلًا، " + f"و{len(warns)} فصلًا محروسًا بلا مصادر — فشل.") + return 1 + print(f"✅ البيان سليم ({len(chapter_files)} فصلًا مسجَّلًا).") + return 0 # ── (أ) حارس القفل: لا كتم صامت ───────────────────────────────────────────────── -def cmd_guard_lock(manifest: dict, base_path: str, changed_path: str) -> int: - """يرفض تقدّم بصمةٍ في القفل دون تعديل الفصل المرتبط في نفس الـPR.""" +def cmd_guard_lock(manifest: dict, base_path: str, changed_path: str, + base_manifest_path: str | None = None) -> int: + """يرفض تقدّمَ بصمةٍ **أو إسقاطَها** دون تعديل الفصل المرتبط في نفس الـPR.""" + if not LOCKFILE.exists(): + print("❌ لا يوجد sync/sources.lock.json — الحارسُ لم يُقَس. " + "شغّل `--update` أوّلًا.") + return 1 cur = json.loads(LOCKFILE.read_text(encoding="utf-8")).get("sources", {}) base_file = Path(base_path) if not base_file.exists() or not base_file.read_text(encoding="utf-8").strip(): @@ -245,21 +427,43 @@ def cmd_guard_lock(manifest: dict, base_path: str, changed_path: str) -> int: # (AR) القاعدة: **كلّ** فصلٍ يستشهد بالمصدر يجب أن يُعدَّل، لا أيّ فصلٍ منها. # المفتاحُ المشترك (`scripts/codegen` بين codegen.md وphilosophy.md مثلًا) # كان يمرّ بتعديل أحدهما فَيَسِمُ الآخرَ طازجًا دون أن يقرأه أحد — أي أنّ الحارسَ - # نفسَه كان يكتم ما بُني لِيَكشِفَه. (أربعة مفاتيحَ مشتركةٍ اليوم.) + # نفسَه كان يكتم ما بُني لِيَكشِفَه. (عددُ المفاتيح المشتركة يُشتقّ من + # البيان عند التشغيل — لا يُكتب هنا رقمًا يتقادم.) + # (AR) الإسقاطُ كتمٌ صامتٌ أيضًا، من البابِ المقابل: بدل تقديمِ البصمةِ بلا + # مراجعة، إزالتُها بلا مراجعة. إسقاطُ مصدرٍ من البيان ثمّ `--update` + # كان يقتل الكاشفَ لفصولٍ كاملةٍ والبوّابتان خضراوان — قِيس بإسقاط + # `.#names` فمرّ الحارسُ بـ«0 بصمةٍ تقدّمت» وثلاثةُ فصولٍ فقدت دعواها. + dropped = sorted(k for k in base if k not in cur) + base_manifest = None + if base_manifest_path: + raw = Path(base_manifest_path).read_text(encoding="utf-8") + if raw.strip(): + base_manifest = yaml.safe_load(raw) offenders = [] for key in advanced + added: chapters = chapters_for_key(manifest, key) untouched = [c for c in chapters if c not in changed_files] if untouched or not chapters: offenders.append((key, chapters, untouched)) + for key in dropped: + # (AR) الفصولُ تُقرأ من **بيانِ الأساس**: المفتاحُ المُسقَط لا أثرَ له في + # البيان الحاليّ، فقياسُه عليه يعطي قائمةً فارغةً دائمًا. + if base_manifest is None: + offenders.append((f"{key} (أُسقِط)", [], + ["<بيانُ الأساس غير مُمرَّر — تعذّر القياس>"])) + continue + was = chapters_for_key(base_manifest, key) + untouched = [c for c in was if c not in changed_files] + if untouched or not was: + offenders.append((f"{key} (أُسقِط)", was, untouched)) if offenders: - print("❌ كتمٌ صامت مرصود: تقدّمت بصماتٌ في القفل دون تعديل فصولها:\n") + print("❌ كتمٌ صامت مرصود: تغيّرت بصماتٌ في القفل دون تعديل فصولها:\n") for key, chapters, untouched in offenders: if not chapters: - print(f" • المصدر `{key}` تقدّم، ولا فصلَ يستشهد به أصلًا!") + print(f" • المصدر `{key}` تغيّر، ولا فصلَ يستشهد به أصلًا!") continue shared = [c for c in chapters if c not in untouched] - print(f" • المصدر `{key}` تقدّم، ولم يُعدَّل: " + print(f" • المصدر `{key}` تغيّر، ولم يُعدَّل: " f"{', '.join(untouched)}") if shared: print(f" (مشتركٌ مع فصولٍ عُدّلت: " @@ -267,8 +471,8 @@ def cmd_guard_lock(manifest: dict, base_path: str, changed_path: str) -> int: print("\nالقاعدة: لا تثبّت بصمةً جديدة (`--update`) إلّا بعد مراجعة الفصل " "المرتبط وتعديله فعليًّا. راجع الفصل، عدّله، ثم أعِد --update.") return 1 - print(f"✅ الحارس مرّ: كل بصمةٍ تقدّمت ({len(advanced)+len(added)}) رافقها " - "تعديلُ **كلّ** فصلٍ يستشهد بها.") + print(f"✅ الحارس مرّ: كل بصمةٍ تقدّمت ({len(advanced)+len(added)}) " + f"أو أُسقِطت ({len(dropped)}) رافقها تعديلُ **كلّ** فصلٍ يستشهد بها.") return 0 @@ -351,6 +555,8 @@ def main() -> int: help="حارس CI: امنع تقدّم القفل دون تعديل الفصل") ap.add_argument("--base", help="مع --guard-lock: ملف قفل الأساس") ap.add_argument("--changed-files", help="مع --guard-lock: ملفّ بقائمة الملفّات المعدَّلة") + ap.add_argument("--base-manifest", + help="مع --guard-lock: بيانُ الأساس (لقياس المفاتيح المُسقَطة)") args = ap.parse_args() manifest = load_manifest() @@ -361,7 +567,8 @@ def main() -> int: if args.guard_lock: if not (args.base and args.changed_files): sys.exit("‏--guard-lock يستلزم --base و--changed-files") - return cmd_guard_lock(manifest, args.base, args.changed_files) + return cmd_guard_lock(manifest, args.base, args.changed_files, + args.base_manifest) if args.update: shas, missing = collect_shas(manifest, ref) write_lock(manifest, ref, shas, args.set_version) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9b31225..50fd2d1 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -8,8 +8,8 @@ # الجزء الأول · البدء - [إعداد البيئة والبناء](getting-started/setup.md) -- [أوّل مساهمة (Walkthrough)](getting-started/first-contribution.md) - [خريطة المستودع](getting-started/repo-map.md) +- [أوّل مساهمة (Walkthrough)](getting-started/first-contribution.md) # الجزء الثاني · المعمارية diff --git a/src/architecture/interconnected.md b/src/architecture/interconnected.md index 222039d..81ddeaa 100644 --- a/src/architecture/interconnected.md +++ b/src/architecture/interconnected.md @@ -2,17 +2,20 @@ > **ماذا ستتعلّم:** لماذا لا يوجد «تغيير معزول» في لغة ص، وأيّ ملفّات يَمَسّها كل نوع تغيير. +> 📎 المجلّدات المذكورة أدناه مبصومةٌ **بأسماء مدخلاتها** على `dev` — فظهورُ ملفٍّ +> جديدٍ فيها أو اختفاؤه يُنذر هذا الفصل، ولا يُنذره تعديلُ محتوًى داخلها. + كل ميزة تعبر عدّة أنظمة: مصدر الحقيقة + المُولَّد + المعجمي/النحوي + المفسّر + المترجم + الأخطاء + التوثيق + الاختبارات. تجاهل أحدها يكسر CI أو تجربة المستخدم. ## جدول الأثر (File List) حسب التغيير | التغيير | الملفّات المتأثّرة عادةً | |---------|--------------------------| -| **كلمة مفتاحيّة** | `language-truth/keywords.yaml` → `shared/lexer/generated/keywords_generated.{h,cpp}` (مُولَّد) + `shared/parser/src//` + `shared/ast/include/` + `interpreter/src/visitors/` + `compiler/src/frontend/` (+opcode في `sir_types.h`) + اختبار `.ص` | -| **دالة مضمنة** | `language-truth/builtins/.yaml` (+`_index.yaml`) → مُولَّد `builtin_registry_generated.h` + `interpreter/src/builtins/` + `compiler/src/backend/llvm/builders/builtins/` + اختبار | +| **كلمة مفتاحيّة** | `language-truth/keywords.yaml` → `shared/lexer/generated/keywords_generated.{h,cpp}` (مُولَّد) + `shared/parser/src//` + `shared/ast/include/` + `interpreter/src/visitors/` (٣٠ ملفَّ `.cpp` في المجلّد — لا كلُّها يمسُّها كلُّ تغيير) + `compiler/src/frontend/` (+opcode في `sir_types.h`) + اختبار `.ص` | +| **دالة مضمنة** | `language-truth/builtins/.yaml` (+`_index.yaml`) → مُولَّد `shared/builtins/generated/builtin_registry_generated.h` + `interpreter/src/builtins/` + `compiler/src/backend/llvm/builders/builtins/` (٩ مدخلات: ٨ `.cpp` + `README.md`) + اختبار | | **رمز خطأ** | `language-truth/errors/.yaml` (مصدر) + `shared/errors/include/error_codes.h` + مُولَّد + اختبار | | **توجيه `@`** | `language-truth/directives.yaml` + مُولَّد + parser + AST + visitors + codegen + اختبار | -| **قاعدة نحويّة** | `language-truth/grammar/*.yaml` (SoT) + `shared/parser/src/` + توثيق مُولَّد `docs/parser_rule/_generated/` | +| **قاعدة نحويّة** | `language-truth/grammar/*.yaml` (SoT) + `shared/parser/src/` + توثيق مُولَّد `docs/parser_rule/_generated/` (٨ أقسام + `INDEX.md`) | | **opcode SIR** | `compiler/include/frontend/sir_types.h` + `SIRBuilder` + `compiler/src/backend/llvm/` + اختبار | ## مخطّط التشابك (مثال: كلمة مفتاحيّة) diff --git a/src/architecture/overview.md b/src/architecture/overview.md index 0d6d26f..842e29a 100644 --- a/src/architecture/overview.md +++ b/src/architecture/overview.md @@ -11,8 +11,9 @@ | المترجم | `compiler/` | AST → SIR → LLVM IR → ملفّ تنفيذيّ (SIR يدعم تعليمات ملكية) | | الخلفيّة الأصليّة | `compiler/include/backend/native/` | SIR → شيفرة آلة → ELF64 ساكن بلا LLVM ولا رابطٍ أجنبيّ — [الفصل](../backend/native.md) | | ~~الآلة الافتراضية~~ | — | `vm/` أُزيل من الشجرة بالإيداع `bcf0a746` («ستُعاد كتابتها من الصفر») — لا فصل له حتّى تُكتب | -| المكتبة القياسية | `stdlib/` | وحدات عربية: core/io/math/string/network/graphics | -| الأدوات | `tools/` | ١٥ مجلّدًا على `dev`: analyze · apk_builder · build · check · compiler (واجهة `sad-build`) · formatter · hub (موزِّع `sad`) · installers · lsp · pkg · profiler · repl · security-scanner · shared · wasm | +| المكتبة القياسية | `stdlib/` | ثماني وحدات `.ص` عربيّة في الجذر + سبعةَ عشرَ مجلّدَ دعمٍ C++ — التعدادُ في [خريطة المستودع](../getting-started/repo-map.md) | +| الرسومات | `features/graphics/` | SadUI: محرّك التخطيط ومفاتيح الخصائص — **ليست في `stdlib/`** — [الفصل](../systems/sadui-layout.md) | +| الأدوات | `tools/` | ١٥ مجلّدًا على `dev`؛ منها `compiler` (واجهة `sad-build`) و`hub` (موزِّع `sad`) — التعدادُ في [خريطة المستودع](../getting-started/repo-map.md) | | مصدر الحقيقة | `language-truth/` | YAML SoT لكل بيانات اللغة + القواعد | ## القاعدة الطبقيّة (CW-02) diff --git a/src/contributing/definition-of-done.md b/src/contributing/definition-of-done.md index 154c971..4ff753a 100644 --- a/src/contributing/definition-of-done.md +++ b/src/contributing/definition-of-done.md @@ -12,8 +12,9 @@ ## التنفيذ المزدوج والاختبار - [ ] الدعم مضاف في **المفسّر والمترجم** (أو `@skip_compiler` موثّق بسبب صريح). - [ ] **اختبار `.ص`** جديد (إيجابيّ + سلبيّ) بصيغة `@expected` الصحيحة. -- [ ] `runner.py --level P0` (وقسم الميزة) يمرّ **100%**. -- [ ] `runner.py --level P1` يمرّ **قبل أي PR** — لا تراجع (BF-29). +- [ ] `python tests/runner.py --level P0` (وقسم الميزة) يمرّ **100%**. +- [ ] `python tests/runner.py --level P1` يمرّ **قبل أي PR** — لا تراجع (BF-29). +- [ ] الثنائيّان مبنيّان في **تهيئةٍ واحدة** — `tests/config.yaml` يقرأ كليهما من `build/bin/Debug/`. - [ ] `sad-build` يبني بلا أخطاء، و`sad-run` يعمل بلا تراجع. ## الجودة والتوافق diff --git a/src/contributing/freshness.md b/src/contributing/freshness.md index 9c76746..ed49b94 100644 --- a/src/contributing/freshness.md +++ b/src/contributing/freshness.md @@ -20,14 +20,48 @@ chapters: - language-truth/keywords.yaml ``` -المصدر إمّا **مسارٌ كامل** (ملف أو مجلد، يُرصد أيّ تغيّر تحته) أو **كائن `{path, lines}`** -يبصم **نطاقًا بعينه** فقط. النطاق يقلّل **الإنذارات الكاذبة** في الملفّات الكبيرة -(تعديلٌ خارج المنطقة الموثَّقة لا يُطلِق إنذارًا) ويرصد **تعفّن** المنطقة المقصودة تحديدًا. +للمصدر **أربعةُ أنماطِ بصم**، ولكلٍّ دعوًى يطابقها: + +| النمط | الصيغة | يرصد | لا يرصد | +|---|---|---|---| +| مسارٌ كامل | `path/to/file` | أيّ تغيّرٍ في الملفّ/المجلّد | — | +| نطاقُ أسطر | `{path, lines: "10-90"}` | تعفّنَ المنطقة الموثَّقة | تعديلًا خارجها | +| **أسماءُ المدخلات** | `{path, names: true}` | ظهورَ مدخلٍ أو اختفاءَه أو تبدّلَ نوعه | تعديلَ محتوًى داخل المجلّد | +| **مصدرٌ داخليّ** | `self:path` | تغيّرَ أدواتِ الدليل نفسِها (بلا شبكة، ويُقاس في الـPR) | — | + +> بصمةُ `self:` تُحسب على الأسطر مُطبَّعةً (`\n`)، فلا تنقلب بتبديل CRLF/LF بين +> ويندوز ولينكس — مقيسٌ لا مفترَض. وبصمةُ `names` الداخليّة **تُشتقّ من `git +> ls-tree` لا من مسحِ القرص**، فلا تدخلها مخرجاتُ البناء ولا `.venv` ولا ملفّاتُ +> خطوةِ الحارسِ نفسِها (قِيس: جذرُ مستودعِ اللغة ٤٣ مدخلًا في git و٧٠ على القرص). +> +> وقاعدةٌ تسري على الأنماط كلِّها: **العدمُ ليس قياسًا.** مجلّدٌ فارغ · نطاقٌ خارجَ +> الملفّ · ردٌّ فارغٌ من `gh` · مجلّدٌ تجاوز سقفَ ١٠٠٠ مدخلٍ في `contents` API — +> كلُّها تُقرأ «متعذّرًا» يحمرّ، لا بصمةً تُثبَّت. لولا ذلك لأنتجت هذه الحالاتُ +> **بصمةً واحدةً مشتركة** (`sha256:e3b0c442…`)، فيحمرُّ الفحصُ مرّةً ثمّ يثبّتها +> `--update`، ويبقى المصدرُ أخضرَ إلى الأبد وهو غيرُ مقيسٍ أصلًا. + +النطاق يقلّل **الإنذارات الكاذبة** في الملفّات الكبيرة (تعديلٌ خارج المنطقة الموثَّقة لا +يُطلِق إنذارًا) ويرصد **تعفّن** المنطقة المقصودة تحديدًا. + +نمط **`names`** هو الوحيد الذي يطابق دعوى «هذا المجلّد يحوي كذا وكذا» — وهي دعوى +`repo-map.md` و`overview.md`. المقيسُ يُبيّن لِمَ لا تصلح بصمةُ الشجرة (tree sha) لها: +بين إيداعَين على `dev` تباعدا ٢٥٠ إيداعًا، انقلبت بصمةُ شجرة `tools/` وبقيت أسماءُ +مدخلاتها **كما هي** — أي إنذارٌ أسبوعيٌّ دائمٌ بلا واقعة. وفي المدّة نفسِها التقط +النمطُ في `stdlib/` ما يجب التقاطُه بالضبط: **اختفاء** `async` و`audio3d` و`crypto` +و`embedded` و**ظهور** `جيسون.ص`. ويُكتب `path: "."` لبصم جذر المستودع — فعودةُ `vm/` +أو ظهورُ جزءٍ جديدٍ تُنذر خريطةَ المستودع. + +ونمط **`self:`** يبصم من **هذا المستودع** لا من مستودع اللغة، لأنّ فصلًا كهذا يوثّق +`check_sync.py` و`sync-check.yml` أنفسَهما. وسببُه واقعة: أُصلح افتراضُ المرجع في +الأداة، وبقي هذا الفصلُ يصف الافتراضَ المنقوض في اليوم نفسِه — بلا كاشف. الآن أيُّ +تعديلٍ في الأداتين يُنذر الفصلَ الذي يصفهما. + الحقل `covers_version` يوثّق **حالة اللغة التي رُوجِع الدليل تجاهها**. ولمّا كان `ref: dev` — لا وسمَ إصدارٍ — فصيغتُه اليوم `dev@<إيداع مختصر>` (مثلًا `dev@1138f5e1`)، أدقُّ من رقم semver لأنّها تسمّي **الإيداع** الذي قِيست عنده -البصمات لا إصدارًا مُتخيَّلًا. يُكتَب في البيان والقفل معًا: `--update --set-version` -ينسخه إلى القفل، و`--update` وحدَه يأخذه من البيان. لا يقيّده مخطّطٌ ولا يقرؤه حارس؛ +البصمات لا إصدارًا مُتخيَّلًا. و`--update` **يشتقّه** من `git ls-remote` عند +المرجع المقيس بدل نسخِه من البيان — فلا يصير عددًا منثورًا صادقًا بالمصادفة؛ +و`--set-version` يتقدّم عليه حين تُثبِّت عند وسمِ إصدار. لا يقيّده مخطّطٌ ولا يقرؤه حارس؛ مستهلِكُه الوحيد نصُّ قضيّة `sync-check.yml` الذي يطبعه كما هو. ## 2) كاشف الانجراف — `scripts/check_sync.py` @@ -51,9 +85,13 @@ flowchart LR CHK -->|اختلاف| DRIFT["⚠️ انجراف → فصول متأثّرة"] ``` -## 3) الأتمتة — مربوطة بالإصدارات -`.github/workflows/sync-check.yml` يقيس الانجراف تجاه **آخر وسم إصدار** للغة (لا تجاه -`dev` المتقلّب)، فتعني النتيجة «هل يطابق الدليلُ الإصدارَ المنشور؟». يعمل: +## 3) الأتمتة +`.github/workflows/sync-check.yml` يقيس الانجراف تجاه **مرجع القفل نفسِه** +(`sources.lock.json`.ref — اليوم `dev`)، لأنّ البصمات أُخذت عنده، فقياسُها عند مرجعٍ +سواه يقارن شيئًا بغيره. (كان الافتراضُ «آخر وسم إصدار»، فأعلن التقريرُ عند `v1.0.0` +حذفَ ٣٠ مسارًا وتعذّرَ ٤٧ — وكلّها قائمةٌ سليمةٌ على `dev` — فصار الإنذارُ ضجيجًا لا +يُقرأ؛ القضيّة #1 شاهدُه.) ولقياس الدليل تجاه إصدارٍ منشور: أعِد البصم عند وسمِه +(`--ref vX --update --set-version X`) فيصير هو مرجعَ القفل. يعمل: - **عند إصدار لغة جديد** (`repository_dispatch: language-release` يطلقه مستودع اللغة)، - **أسبوعيًّا** (شبكة أمان)، و**يدويًّا** (مع تحديد ref اختياريّ). @@ -64,8 +102,28 @@ flowchart LR الكشف وحده لا يكفي؛ يلزم منعُ الالتفاف عليه. حارسان في CI (`ci.yml`) على كل PR: - **حارس القفل (لا كتم صامت):** يرفض تقدّم أيّ بصمةٍ في `sources.lock.json` **ما لم يُعدَّل الفصل المرتبط بها في نفس الـPR**. فلا يصير `--update` زرَّ إسكاتٍ بلا مراجعة. -- **حارس البيان:** يتحقّق (بلا شبكة) من وجود ملفّات الفصول، صحّة نطاقات الأسطر، وأنّ - **كل فصل تقنيّ في `SUMMARY` مسجَّلٌ** مصادرُه — فلا يَفلت فصلٌ جديد من المظلّة. + ويقيس **الاتّجاهين**: التقدّمَ و**الإسقاط**. فإزالةُ مصدرٍ من البيان ثمّ `--update` + كتمٌ صامتٌ من البابِ المقابل — يُقتَل الكاشفُ لفصولٍ كاملةٍ بلا مراجعةِ حرف. ولمّا + كان المفتاحُ المُسقَط لا أثرَ له في البيان الحاليّ، تُقرأ فصولُه من **بيان + الأساس** عبر `--base-manifest`؛ وإن لم يُمرَّر، احمرّ الحارسُ ولم يمرّ: الحارسُ + الذي لا يجد ما يقيس به لا يقول «مرّ». +- **حارس البيان:** يتحقّق (بلا شبكة) من وجود ملفّات الفصول، صحّة نطاقات الأسطر، + وسلامةِ النمطين الجديدين (`names` و`lines` لا يجتمعان؛ ومصدرُ `self:` موجودٌ فعلًا)، + وأنّ **كلّ فصلٍ في `SUMMARY`** تحت الأقسام السبعة `frontend/` · `backend/` · + `systems/` · `sot/` · `architecture/` · `getting-started/` · `contributing/` + له مصادرُ. كانت المظلّةُ أربعةَ أقسامٍ فقط، فبقيت الثلاثةُ الأخيرة — وهي حاملةُ + دعاوى الأوامرِ التي يُشغّلها القارئ — **خارجها كلّيًّا** حتّى تعفّنت. (المرجع: + `GUARDED_SECTIONS` في `scripts/check_sync.py`.) + صفحاتُ الجذر (`introduction` · `glossary` · `status` · `SUMMARY`) خارج المظلّة + عمدًا: دعاواها عن الدليل لا عن اللغة. ومنها ما يُسجَّل **طوعًا** حين يحمل دعوًى + مقيسة — كـ`glossary.md` وأسماءِ الثنائيّات. + ويقيس هذا الحارسُ كذلك **تعفّنَ مصادر `self:` في نفس الـPR**: هي بلا شبكة، فلا + عذرَ لتأجيلها إلى الفحص الأسبوعيّ. فلو عُدِّل `check_sync.py` وحده دون تثبيتٍ + ومراجعةِ هذا الفصل، احمرّ الـPR — لا مرّ أخضرَ وفصلُه يصفُ سلوكًا منقوضًا (وهي + عينُ الواقعة التي وُلد منها النمط). و**غيابُ القفل نفسِه خطأٌ صريح** في + `--validate` و`--guard-lock` كليهما: حذفُ ملفٍّ واحدٍ كان يُطفئ البوّابةَ صامتةً + وخضراء. وحين يَنقص البيانُ فصلًا محروسًا، يُختَم السجلُّ بـ`❌` لا بـ`✅` — + لأنّ مَن يمسح سجلَّ CI بعينه يقرأ آخرَ سطرٍ حكمًا. ## 5) العقد عبر المستودعين العقد يجب أن يصل لمن **يغيّر اللغة فعلًا** (في مستودع اللغة، لا هنا): diff --git a/src/getting-started/first-contribution.md b/src/getting-started/first-contribution.md index 9d2b1aa..c3fc268 100644 --- a/src/getting-started/first-contribution.md +++ b/src/getting-started/first-contribution.md @@ -19,7 +19,7 @@ cd /c/s_lang/temp-brunch/builtin-جذر ## 3) ابدأ من مصدر الحقيقة (لا من الكود المُولَّد) أضف الدالة إلى `language-truth/builtins/.yaml`، ثم أعد التوليد: ```bash -python scripts/codegen/gen_all_builtins_yaml.py # أو المولّد المعنيّ +python x.py gen # أو مولِّدٌ بعينه: scripts/codegen/gen_all_builtins_yaml.py ``` ## 4) نفّذ في الطبقة الصحيحة @@ -27,13 +27,14 @@ python scripts/codegen/gen_all_builtins_yaml.py # أو المولّد المع - المترجم: `compiler/src/backend/llvm/builders/builtins/*.cpp`. ## 5) اكتب اختبار `.ص` (إيجابيّ + سلبيّ) -ملف تحت `tests/` بصيغة `@expected` الصحيحة. +ملف `.ص` تحت **`tests/behavior/`** (لا تحت `tests/` مباشرةً) بصيغة `@expected` +الصحيحة — و`tests_dir: tests/behavior` في `tests/config.yaml` هو ما يجعل الـrunner +يراه. راجع [خريطة المستودع](repo-map.md) للحِزَم الستّ وأيُّها يناسب اختبارَك. ## 6) ابنِ وشغّل (تنفيذ مزدوج) ```bash -cmake --build build --config Debug --target sad-run -cmake --build build --config Release --target sad-build -python runner.py --level P1 # يجب أن يمرّ 100% بلا تراجع +python x.py build # المحرّكان معًا في تهيئة واحدة +python tests/runner.py --level P1 # يجب أن يمرّ 100% بلا تراجع ``` ## 7) أودِع (موقّع GPG) وافتح PR @@ -54,4 +55,4 @@ git branch -D agent/builtin-جذر > ✅ راجع [معيار الإنجاز](../contributing/definition-of-done.md) قبل إعلان الانتهاء. --- -**اقرأ بعده:** [خريطة المستودع](repo-map.md). +**اقرأ بعده:** [نظرة عامّة على الطبقات](../architecture/overview.md). diff --git a/src/getting-started/repo-map.md b/src/getting-started/repo-map.md index 0373ca5..7d4ce95 100644 --- a/src/getting-started/repo-map.md +++ b/src/getting-started/repo-map.md @@ -2,8 +2,12 @@ > **ماذا ستتعلّم:** أين يعيش كل شيء في `sadlang/s-programming-language`. +> 📎 مقيسٌ على `dev` — الأسماء والأعداد أدناه من شجرة المستودع نفسها لا من الذاكرة. + ``` s-programming-language/ +├── x.py ← البوّابة الموحّدة: سبعةُ أوامر (build · test · gen · +│ verify · configure · conformance · clean) ├── shared/ ← النواة المشتركة │ ├── lexer/ ← المحلل المعجمي + Token + Position │ ├── parser/ ← المحلل النحوي (recursive descent) @@ -13,31 +17,55 @@ s-programming-language/ ├── interpreter/ ← المفسّر الشجريّ (InterpreterCore + visitors + builtins) ├── compiler/ ← المترجم: AST → SIR → LLVM IR → تنفيذيّ │ ├── src/frontend/ ← SIRBuilder + sir_types.h (opcodes الملكية) -│ └── src/backend/llvm/ ← LLVMCodeGen + builders -├── stdlib/ ← المكتبة القياسية (core/io/math/string/network/graphics) +│ ├── src/backend/llvm/ ← LLVMCodeGen + builders +│ └── include/backend/native/ +│ ← الخلفيّة الأصليّة (بلا LLVM) +├── stdlib/ ← المكتبة القياسية: وحدات `.ص` في الجذر + مجلّدات دعم C++ +├── features/ ← أنظمةٌ كبرى مستقلّة (منها SadUI للرسومات) ├── runtime/ ← ABI/FFI المستقلّ + الوضع الحرّ (freestanding) -├── tools/ ← analyze · build · check · compiler(واجهة sad-build) · formatter · hub · lsp · pkg · profiler · repl · security-scanner · wasm · apk_builder +├── tools/ ← ١٥ مجلّدَ أدوات (sad-build · sad · lsp · repl …) ├── language-truth/ ← ⭐ مصدر الحقيقة الموحّد (YAML) │ ├── keywords.yaml · operators.yaml · types.yaml · directives.yaml │ ├── builtins/ · errors/ · grammar/ ← قواعد الإنتاج (SoT) │ └── _schemas/ ← مخطّطات JSON للتحقّق ├── scripts/codegen/ ← gen_*.py (تقرأ YAML وتُنتج C++/توثيق) ├── docs/ ← توثيق (incl. parser_rule/_generated المُولَّد) -├── tests/ ← اختبارات .ص + rules_matrix + comprehensive +├── examples/ ← أمثلة `.ص` مرقّمة (`01_مرحبا.ص` … `07_مكوّن_مركّب.ص`) +├── tests/ ← `runner.py` · `config.yaml` · `framework/` · `metrics/` +│ └── behavior/ ← ⟵ هنا تعيش حِزَم `.ص` الستّ (انظر الجدول أدناه) ├── _bmad-output/ ← نظام الحوكمة (سياسات/ستوريات/قرارات) └── .github/skills/ ← مهارات الوكلاء (sad-lang-dev …) ``` + +### ماذا في `stdlib/` و`features/` و`tests/behavior/` +هذه القوائمُ **خارج كتلة الكود عمدًا**: كتلُ الكود تُعرَض LTR (`theme/rtl.css`)، +فينقلبُ فيها ترتيبُ أيّ سلسلةٍ عربيّةٍ أمامَ عين القارئ. + +| المجلّد | ما فيه | +|------|------| +| `stdlib/` — وحدات `.ص` | رياضيات · نصوص · مصفوفات · خرائط · ملفات · شبكات · جيسون · وقت | +| `stdlib/` — مجلّدات دعم C++ (١٧) | `io` · `math` · `string` · `json` · `xml` · `database` · `filesystem` · `image` · `include` · `low_level` · `platform` · `system` · `test` · `freestanding` · `إضافات` · `نص` · `ويب` | +| `features/` | `graphics` (SadUI: التخطيط ومفاتيح الخصائص) · `network` — **الرسومات ليست في `stdlib`** | +| `tools/` (١٥) | `analyze` · `apk_builder` · `build` · `check` · `compiler` (واجهة `sad-build`) · `formatter` · `hub` (موزِّع `sad`) · `installers` · `lsp` · `pkg` · `profiler` · `repl` · `security-scanner` · `shared` · `wasm` | +| `tests/behavior/` (٦ حِزَم) | `P0_smoke` · `sections` · `rules_matrix` · `grammar_gaps` · `null_safety` · `_regression` | + +> ⚠️ `rules_matrix` **تحت `tests/behavior/`** لا تحت `tests/` مباشرةً — ولهذا يكتب CI +> `--dir rules_matrix` بعد أن يقرأ `tests_dir: tests/behavior` من `config.yaml`. +> ولا وجود لمجلّد `tests/comprehensive`. + ## ملفّات تُقرأ أولًا | الملف | لماذا | |------|------| | `shared/lexer/include/token.h` | أنواع الرموز و`Position` | -| `shared/lexer/src/lexer_keywords.cpp` | تسجيل الكلمات المحجوزة (40) | +| `language-truth/keywords.yaml` | معجم اللغة كلُّه: المحجوزُ والعواملُ اللفظيّة والسياقيُّ والأنواعُ المضمنة — ومنه يُولَّد المعجم | +| `shared/lexer/src/lexer_keywords.cpp` | تسجيل الكلمات المحجوزة (المولَّد منها) | | `shared/types/include/value.h` | نوع القيم في وقت التشغيل | | `shared/parser/include/parser_core.h` | واجهة المحلل (كل دوال `parse*`) | | `interpreter/include/core/interpreter_core.h` | نقطة دخول المفسّر | | `compiler/include/frontend/sir_types.h` | تعليمات/أنواع SIR | | `language-truth/README.md` | مصدر الحقيقة | +| `tests/config.yaml` | مسارا الثنائيَّين اللذان يقيس بهما الـrunner | --- -**اقرأ بعده:** [نظرة عامّة على الطبقات](../architecture/overview.md). +**اقرأ بعده:** [أوّل مساهمة](first-contribution.md). diff --git a/src/getting-started/setup.md b/src/getting-started/setup.md index ce6ccf5..8282114 100644 --- a/src/getting-started/setup.md +++ b/src/getting-started/setup.md @@ -2,11 +2,22 @@ > **ماذا ستتعلّم:** كيف تجلب المستودع، تهيّئ الأدوات، وتبني المفسّر والمترجم. +> 📎 المصدر: [`CMakeLists.txt`](https://github.com/sadlang/s-programming-language/blob/dev/CMakeLists.txt) · +> [`tests/config.yaml`](https://github.com/sadlang/s-programming-language/blob/dev/tests/config.yaml) + ## المتطلبات - **C++17** ومُصرِّف حديث (MSVC على Windows، أو Clang/GCC). -- **CMake ≥ 3.20**. -- **LLVM 18** — *اختياريّ*، للمترجم `sad-build` فقط (`ENABLE_LLVM_BACKEND=ON`). -- **Python 3** — لمولّدات الكود (`scripts/codegen/gen_*.py`) و`runner.py`. +- **CMake ≥ 3.15** — هذا ما يشترطه `cmake_minimum_required` فعلًا، لا 3.20. +- **LLVM 18** — للمترجم `sad-build`. الخيار `ENABLE_LLVM_BACKEND` **افتراضيّه + `ON`**، فلا تحتاج تمريره. ودقّق: هدفُ `sad-build` **يُعرَّف داخل** + `if(ENABLE_LLVM_BACKEND AND LLVM_FOUND)` في `apps/CMakeLists.txt` — فبلا LLVM + لا يوجد الهدفُ أصلًا، ويسقط `x.py build` معه، ولا يُنتَج + `build/bin/Debug/sad-build.exe` الذي يشترطه `tests/config.yaml` **أبدًا**. + البديلُ الحقيقيّ هو الهدفُ المستقلّ **`sad-build-native`** (المترجم النحيل، بلا + LLVM، في كلّ التهيئات) — راجع [فصل LLVM](../backend/llvm.md) و[الخلفيّة + الأصليّة](../backend/native.md). فإن لم تنوِ استعمالَ الخلفيّة الأصليّة، + اعتبِر LLVM **شرطًا** لا خيارًا. +- **Python 3** — لمولّدات الكود (`scripts/codegen/gen_*.py`) و`tests/runner.py`. - **Git + GPG** — للمساهمة (الفروع المحميّة تشترط توقيع GPG). ## الجلب @@ -15,44 +26,63 @@ git clone https://github.com/sadlang/s-programming-language.git cd s-programming-language ``` -## البناء (PowerShell على Windows) +## البناء +الطريق المُوصى به هو البوّابة الموحّدة `x.py` — تبني المحرّكين **معًا لكلّ تهيئة**، +فلا تقع في اختلاف التهيئتين الموصوف تحت: +```bash +python x.py build # بناء المحرّكين +python x.py test # بناء + تحقّق + تشغيل الـrunner +python x.py gen # توليد المصدر من language-truth/ +``` +(الأوامر الأخرى: `configure` · `verify` · `conformance` · `clean`.) + +وإن بنيتَ بـCMake مباشرةً (PowerShell على Windows): ```powershell -cmake -S . -B build # تهيئة أولى -cmake --build build --config Debug --target sad-run # المفسّر (أسرع) -cmake --build build --config Release --target sad-build # المترجم (LLVM) -cmake --build build --config Debug # كل شيء +cmake -S . -B build # تهيئة أولى +cmake --build build --config Debug --target sad-run # المفسّر +cmake --build build --config Debug --target sad-build # المترجم +cmake --build build --config Debug # كل شيء ``` -> ⚠️ **ثلاثة فخاخ:** +> ⚠️ **فخاخُ البناء:** +> - **ابنِ الاثنين في تهيئةٍ واحدة، وأعِد بناءهما معًا.** `tests/config.yaml` يقرأ +> المسارَين من **`build/bin/Debug/`** كليهما. فإن بنيتَ `sad-build` في `Release` +> لم يجده الـrunner، وإن أعدتَ بناءَ أحدهما وحدَه قِستَ ثنائيًّا بائتًا. وهذه +> بعينها عثرةُ «مفسّر Debug + مترجم Release» التي **يقول تعليقُ +> `tests/config.yaml` نفسِه** إنّها أُزيلت جذريًّا في `cmake/llvm.cmake`؛ +> لا تُعِدها بيدك. > - **`sadc.exe` اسمٌ متقاعد — لا يُنتجه أيّ هدف.** اسمُ الهدف مُوحَّدٌ مع اسم -> المُخرَج: `sad-run` ⇒ `sad-run.exe`، و`sad-build` ⇒ `sad-build.exe` -> ([`tests/config.yaml`](https://github.com/sadlang/s-programming-language/blob/dev/tests/config.yaml)). -> فلا تنسخ ثنائيًّا باسمٍ آخر لأجل `runner.py` — فهو يقرأ المسارَين من ذلك الملفّ. +> المُخرَج: `sad-run` ⇒ `sad-run.exe`، و`sad-build` ⇒ `sad-build.exe`. +> فلا تنسخ ثنائيًّا باسمٍ آخر لأجل الـrunner — فهو يقرأ المسارَين من ذلك الملفّ. > - `sad.exe` ليس المفسّر بل **موزِّع أوامرٍ (hub)** يُشغّل الأدواتِ عمليّاتٍ فرعيّة؛ > المفسّرُ الفعليّ `sad-run.exe`، فاستدعِه مباشرةً. -> - الـrunner يقرأ تهيئتَي الثنائيَّين من `tests/config.yaml` — أعد بناءهما معًا -> حتّى لا تقيس ثنائيًّا بائتًا. ## التشغيل +الأمثلة الفعليّة تحت `examples/` بأسماء عربيّة مرقّمة (`01_مرحبا.ص` … `07_مكوّن_مركّب.ص`): ```powershell -.\build\bin\Debug\sad-run.exe examples\test_simple.ص # تفسير -.\build\bin\Release\sad-build.exe examples\test_simple.ص # ترجمة لملف تنفيذيّ +.\build\bin\Debug\sad-run.exe examples\01_مرحبا.ص # تفسير +.\build\bin\Debug\sad-build.exe examples\01_مرحبا.ص # ترجمة لملف تنفيذيّ ``` ## الاختبارات -الاختبارات **معطّلة افتراضيًّا**؛ فعّلها بـ`-DBUILD_TESTS=ON`. للتنفيذ المزدوج (مفسّر + مترجم) استخدم `runner.py`: +الاختبارات **معطّلة افتراضيًّا** (`option(BUILD_TESTS ... OFF)`)؛ فعّلها بـ`-DBUILD_TESTS=ON`. +للتنفيذ المزدوج (مفسّر + مترجم) استخدم **`tests/runner.py`** — لا يوجد `runner.py` +في جذر المستودع، وهذا هو الاستدعاء الذي يستعمله CI نفسه: ```bash -python runner.py --level P0 # الحزمة الأساسيّة -python runner.py --level P1 # المطلوبة قبل أي PR (لا تراجع) +python tests/runner.py --level P0 # الحزمة الأساسيّة +python tests/runner.py --level P1 # المطلوبة قبل أي PR (لا تراجع) ``` +(و`python x.py test` يبني المحرّكين ثمّ يستدعي هذا الـrunner نفسَه — استعمِله حين +تريد البناءَ والقياسَ في خطوة، والاستدعاءَ المباشرَ حين يكون البناءُ حاضرًا.) ## توليد الكود من مصدر الحقيقة بعد تعديل أي YAML في `language-truth/`: ```bash -python scripts/codegen/gen_keywords.py # مثال: الكلمات المفتاحيّة -python scripts/codegen/gen_parser_grammar_docs.py # توثيق القواعد +python x.py gen # كلُّ المولّدات دفعةً واحدة +python scripts/codegen/gen_keywords.py # أو مولِّدٌ بعينه +python scripts/codegen/gen_parser_grammar_docs.py # توثيق القواعد ``` > راجع [توليد الكود](../sot/codegen.md). --- -**اقرأ بعده:** [أوّل مساهمة](first-contribution.md). +**اقرأ بعده:** [خريطة المستودع](repo-map.md). diff --git a/src/glossary.md b/src/glossary.md index af6a842..7057617 100644 --- a/src/glossary.md +++ b/src/glossary.md @@ -18,10 +18,11 @@ | **DoD** | Definition of Done — معيار اعتبار التغيير منجَزًا. | | **worktree** | فرع git في مجلد منفصل يشارك نفس المستودع (`C:/s_lang/temp-brunch/`). | | **CW-NN / BF-NN / GR-NN** | قواعد كتابة الكود / إصلاح الأخطاء / الحوكمة (مراجع معياريّة). | -| **sad-build** | مُترجِم لغة ص (AST → SIR → LLVM → تنفيذيّ). هدفُ CMake ومُخرَجُه `sad-build.exe`. | -| **sad-run** | المفسّر الشجريّ (`sad-run.exe`). | +| **sad-build** | مُترجِم لغة ص (AST → SIR → LLVM → تنفيذيّ). هدفُ CMake ومُخرَجُه `sad-build.exe`، ويقرأه الـrunner من `build/bin/Debug/`. | +| **sad-run** | المفسّر الشجريّ (`sad-run.exe`) — من `build/bin/Debug/` كذلك: الثنائيّان في **تهيئةٍ واحدة**. | | **sad** | موزِّع الأوامر (hub) — يُشغّل الأدوات عمليّاتٍ فرعيّة، وليس المفسّر. | | **~~sadc~~** | اسمٌ **متقاعد**: لا هدفَ يُنتجه اليوم. المترجم هو `sad-build`. | +| **`tests/config.yaml`** | مصدرُ مسارَي الثنائيَّين و`tests_dir` — لا تنسخ ثنائيًّا باسمٍ آخر لأجل الـrunner. | --- [العودة للمقدّمة](introduction.md) diff --git a/sync/sources.lock.json b/sync/sources.lock.json index f51c16a..2fc1989 100644 --- a/sync/sources.lock.json +++ b/sync/sources.lock.json @@ -3,8 +3,11 @@ "ref": "dev", "repo": "sadlang/s-programming-language", "sources": { + ".#names": "names:1ccbd21d37ca347e", ".github/BRANCH_PROTECTION_POLICY.md": "2cf24d5c67e9c2c9932ce7e9dae784d85216c4d2", ".github/skills/sad-lang-dev/references/governance.md": "9d6d0e8524cf7c27820fc0f6f25167ab2d702ed3", + "CMakeLists.txt#L29-70": "sha256:78a24c2c7cf95d39", + "apps/CMakeLists.txt#L292-310": "sha256:d208b667973d02f8", "compiler/include/backend/native/elf64_writer.h": "8ebd8626f881e72be4d7b91b32598255a4fb9e0b", "compiler/include/backend/native/sir_lowering_common.h": "259cceb18f6b2fc4ef0e978ee6e9c32ed406903e", "compiler/include/backend/native/sir_native_lowering.h#L1-30": "sha256:9a217df04a7742ee", @@ -13,6 +16,7 @@ "compiler/include/frontend/sir_constants.h": "a194d187bc7c4298d07a1340adfc68ce9877b297", "compiler/include/frontend/sir_types.h": "d7adf45f13c2aa9071f7a8bd36550abeeb5f0fcb", "compiler/src/backend/llvm": "525f452df4773db03c579b2b2f84454361920cbf", + "compiler/src/backend/llvm/builders/builtins#names": "names:c8b7f2f962cb6152", "compiler/src/backend/llvm/builders/builtins/io_builtins_ops.cpp": "169fdb572efd32d02919dc09c5e94b4a621d3468", "compiler/src/backend/llvm/builders/builtins/security_builtins_ops.cpp": "96616d075384d3b2a1cff7773ba63ed721c5b1cb", "compiler/src/backend/llvm/builders/collections/map_ops.cpp": "40b02975f418d9db94ba3314f06a07d539c46472", @@ -21,6 +25,9 @@ "compiler/src/frontend/builders/expression_comp2.cpp": "a857b9752d53eaa9a36c8a9f4ef4d4e03f5c260d", "compiler/src/frontend/builders/expression_comprehensions.cpp": "48284a8c8ceea0663dbd250bbb9b0efa8ac787d8", "compiler/src/frontend/builders/statement_for_range.cpp": "6345f89e25b5f426a8ea4f6a4ca836a50f1b28e4", + "docs/parser_rule/_generated#names": "names:8e43d6199dcb4ce5", + "examples#names": "names:4e8e58a6b7295198", + "features#names": "names:4b0db2883814c7ca", "features/graphics/core/include/sad_ui/prop_keys.h": "149ca3c9abcd815c662f446fff0c9c0d64d6e85a", "features/graphics/core/src/layout.cpp": "b02fcd06d7a1ebfd98904c411cc633043714a45d", "interpreter/include/channel.h": "119dc0e9cfea1b7e1b514a7d2b49614c1e499860", @@ -30,6 +37,7 @@ "interpreter/src/builtins/builtin_module_assertions.cpp": "1cf037f1f5ffb75e9c13aa3709b23e20967f50ef", "interpreter/src/builtins/builtin_module_crypto.cpp": "7bb771626147ce04f7e43a715beec99bafeefc44", "interpreter/src/core/interpreter_core.cpp": "a6d3445646d9843e4b26bba1ef5e95f72600ec7f", + "interpreter/src/visitors#names": "names:da81f693ba6445f2", "interpreter/src/visitors/expression_evaluator_members_advanced.cpp": "bd2003b66b8be9c340e4650b2b851c9348765191", "language-truth": "c96c2155d17c2cae855b72b5ad371b583ea80ecc", "language-truth/_schemas": "113cd3948583dfb130fe4169f18e4ff6c7e16cd7", @@ -45,14 +53,19 @@ "language-truth/types.yaml": "73b5c1d165ec83f73b819c4d6af52333e2c217e4", "language-truth/ui_props.yaml": "89cbf23a799f309698e97b29ecc595c293798963", "scripts/codegen": "54744b199f588b1a93b08ad677a069b263c21233", + "scripts/codegen#names": "names:4135e860113590d5", "scripts/codegen/gen_error_messages.py": "3a70f260e525f133b9a93f9919b3622e9056e871", "scripts/codegen/gen_grammar_lowers_to.py": "3c7466870b2218d12089eea65d2b3361123b11b1", "scripts/codegen/gen_native_diagnostics.py": "f0accb963fc8f296e38c1f1d4226c0c5a5edee29", "scripts/codegen/gen_parser_grammar_docs.py": "7b9f7a8a87252ee4599b4fa6da79cb18bbb05120", "scripts/codegen/gen_ui_props.py": "5f750bc52cbe4e85bb67f087a94d67a840fcc6fe", "scripts/native_backend/run_native_proofs.sh": "84069b1cd325573c7336b131d37997788a4d1885", + "self:.github/workflows/ci.yml": "sha256:3d9b1b76ba0585b0", + "self:.github/workflows/sync-check.yml": "sha256:a22d7b9c3e9ae66d", + "self:scripts/check_sync.py": "sha256:d1ad2b646e901155", "shared/ast/include/ast_visitor.h": "6a19219711e62321697a041044962642888091f5", "shared/ast/include/expressions.h": "8c8ffe93587aa833c10890b9c4e12c2e9855825d", + "shared/builtins/generated#names": "names:0087d3f78e675aaa", "shared/errors/include/error_codes.h": "893d39e3bf05aaeb798d9ff46f64e347fb46dff1", "shared/errors/include/error_manager.h": "63c70109de759eaedf809642d11681a1453db5d3", "shared/lexer/include/token.h": "b899691a3f715415d2cd1780ed0601b0d1ade52e", @@ -70,9 +83,17 @@ "shared/types/include/sad_type_system.h": "abc0ace4395e26799a23ff5346efbe0714515d4b", "shared/types/include/value.h": "4d16c5e5f6c17fd4393f1d1da32716582d15750d", "shared/types/src/class_manager.cpp": "3afe4d8387045573f7ae9d1c19b54eee7bb1226f", + "stdlib#names": "names:df15efe2b81d162d", + "tests#names": "names:c1b294150aba797a", + "tests/behavior#names": "names:e842e431bdc20e99", + "tests/config.yaml#L15-23": "sha256:6756185d98a72012", + "tests/config.yaml#L25-26": "sha256:22a5e208ad3f2032", + "tests/config.yaml#L27": "sha256:9b597def0e032eda", + "tools#names": "names:5839c50dd6aeb34b", "tools/compiler/compiler_driver_android_linker.cpp": "86c4974e713923f47c0f16077b4db8ede94eef28", "tools/compiler/compiler_driver_backend_llvm_absent.cpp": "7ea38eea2362fccc22e6d5d6666a9b4d55e09ac2", "tools/compiler/compiler_driver_native.cpp": "74676054c3660ec7ac0df65a1da9aa8d3baab0dd", - "tools/compiler/runtime/sad_embedded_runtime.c": "c4465bac5594536e01b41f099a6dddf9fc80999d" + "tools/compiler/runtime/sad_embedded_runtime.c": "c4465bac5594536e01b41f099a6dddf9fc80999d", + "x.py#L1310-1350": "sha256:f6301ea3d5272221" } } diff --git a/sync/sources.yaml b/sync/sources.yaml index ab8b444..a207cd9 100644 --- a/sync/sources.yaml +++ b/sync/sources.yaml @@ -157,3 +157,69 @@ chapters: - file: src/contributing/governance.md sources: - .github/skills/sad-lang-dev/references/governance.md + + # الموجة الثالثة: فصولٌ كانت خارج الحارس، ودعاواها أوامرُ يُشغّلها القارئ. + # (AR) نطاقُ `tests/config.yaml` مشطورٌ بحسب الدعوى: 25-26 مسارا الثنائيَّين، + # و27 `tests_dir` وحده. الجمعُ في نطاقٍ واحد (24-27) كان يقرن **خمسةَ** فصول + # بمفتاحٍ واحد، فيلزم حارسُ القفل تعديلَ فصلٍ لا علاقةَ لدعواه بما تغيّر. + - file: src/getting-started/setup.md + sources: + - { path: CMakeLists.txt, lines: "29-70" } # cmake_minimum_required(3.15) + BUILD_TESTS OFF + ENABLE_LLVM_BACKEND ON + - { path: tests/config.yaml, lines: "25-26" } # مسارا الثنائيَّين من build/bin/Debug + - { path: x.py, lines: "1310-1350" } # الأوامر الفرعيّة للبوّابة الموحّدة + - { path: ".", names: true } # دعوى «لا runner.py في الجذر» + - { path: tests, names: true } # دعوى «tests/runner.py هو المستدعى» + - { path: examples, names: true } # دعوى «examples\01_مرحبا.ص» + - { path: apps/CMakeLists.txt, lines: "292-310" } # sad-build داخل شرط LLVM + البديل sad-build-native + - { path: tests/config.yaml, lines: "15-23" } # تعليقُ «Debug+Release» وعلاجُه في cmake/llvm.cmake + + - file: src/getting-started/first-contribution.md + sources: + - { path: tests, names: true } # `python tests/runner.py` — لا runner.py في الجذر + - { path: tests/config.yaml, lines: "27" } # دعوى «الاختبار تحت tests/behavior» + - { path: x.py, lines: "1310-1350" } # `python x.py build` + + - file: src/getting-started/repo-map.md + sources: + - language-truth/keywords.yaml # ٤٠ محجوزة · ٣ · ٤٤ · ١٧ + - { path: tests/config.yaml, lines: "27" } # tests_dir: tests/behavior + - { path: ".", names: true } # شجرة الجذر كما تُرسم في الفصل + - { path: tools, names: true } + - { path: stdlib, names: true } + - { path: tests, names: true } # لا tests/comprehensive + - { path: tests/behavior, names: true } # حِزَم `.ص` الستّ + - { path: examples, names: true } # 01_مرحبا … 07_مكوّن_مركّب + - { path: x.py, lines: "1310-1350" } # الفصل يسمّي الأوامر الفرعيّة في شجرته + + - file: src/contributing/definition-of-done.md + sources: + - { path: tests/config.yaml, lines: "25-26" } # الثنائيّان من تهيئةٍ واحدة + - { path: tests, names: true } # `python tests/runner.py` + + # ── الحلّ الجذريّ: دعاوى «شكل الشجرة» تُبصَم بأسماء المدخلات (names) لا بمحتواها، + # وفصولُ الدليل التي توثّق أدواتِه تُبصَم من هذا المستودع نفسِه (self:). + - file: src/architecture/overview.md + sources: + - { path: ".", names: true } # جذر الشجرة — يرصد عودة vm/ أو ظهور جزءٍ جديد + - { path: tools, names: true } # «١٥ مجلّدًا» بأسمائها + - { path: stdlib, names: true } # وحدات `.ص` + مجلّدات الدعم + - { path: features, names: true } # features/graphics — الرسومات ليست في stdlib + + - file: src/architecture/interconnected.md + sources: + - { path: scripts/codegen, names: true } + - { path: interpreter/src/visitors, names: true } + - { path: compiler/src/backend/llvm/builders/builtins, names: true } + - { path: docs/parser_rule/_generated, names: true } + - { path: shared/builtins/generated, names: true } # دعوى مسار builtin_registry_generated.h + + - file: src/contributing/freshness.md + sources: + - self:scripts/check_sync.py # الفصل يصف هذه الأداة بعينها + - self:.github/workflows/sync-check.yml # ومسارَ الفحص الأسبوعيّ + - self:.github/workflows/ci.yml # والفصلُ يصف حارسَي ci.yml بعينهما + + - file: src/glossary.md + sources: + - { path: tests/config.yaml, lines: "25-26" } # sad-run · sad-build · sadc المتقاعد + - { path: tests/config.yaml, lines: "27" } # دعوى `tests_dir`