From f3511b4692519920e6490d484899edb13359ccea Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sat, 2 May 2026 14:45:09 +0300 Subject: [PATCH 1/3] =?UTF-8?q?docs(closure-plan):=20v2.3=20=E2=80=94=20po?= =?UTF-8?q?st-merge=20status=20update=20for=20Wave=201=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1 PR (closure/wave1-faz9-32) merged into main. Plan tracking state synchronised with reality: - 12 phases (Faz 9, 10, 11, 12, 13, 14, 15, 25, 27, 29, 31, 32) flipped from "✅ Wave 1 PR" to "✅ DONE" — code shipped, in main. - 18 phases (Faz 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 34, 35, 36, 37, 38) flipped from "🔄 Wave 1 IN PROGRESS" to "⏳ PENDING" — Wave 2+ scope. - Faz 33 (release) stays "⏳ POST-WAVE". - Status legend rewritten so the three states are unambiguous and carry the merge date as a fixed reference point. - §16 versioning entry v2.3 added with the count of phases moved. No content phase change; tracking-only. Co-Authored-By: Claude Sonnet 4.6 --- .../code_reviews/closure-plan-202604300906.md | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/docs/analysis/code_reviews/closure-plan-202604300906.md b/docs/analysis/code_reviews/closure-plan-202604300906.md index 4495b1d4..e271612c 100644 --- a/docs/analysis/code_reviews/closure-plan-202604300906.md +++ b/docs/analysis/code_reviews/closure-plan-202604300906.md @@ -2,7 +2,7 @@ **Plan ID:** `closure-plan-202604300906` **Created:** 2026-04-30 -**Last revised:** 2026-05-02 (v2.2 — ghost feature analysis absorption: Faz 34-38 yeni; Wave 1 IN PROGRESS işaretleri) +**Last revised:** 2026-05-02 (v2.3 — Wave 1 PR merge edildi; ✅ Wave 1 PR → ✅ DONE, 🔄 Wave 1 IN PROGRESS → ⏳ PENDING, kalan iş Wave 2+) **Branch:** `main` **Commit (baseline):** `6b515ed912f8f22304194c1b3f55ed07a26f519c` **Source review:** [`master-review-opus-202604300906.md`](./master-review-opus-202604300906.md) @@ -78,43 +78,45 @@ Her faz bir veya birden çok PR'la kapanır. Sıralama dependency-driven; parale | **6** | `forgelm verify-audit` subcommand + library function | Tek PR | 1 | — | ✅ PR #19 | | **7** | `safe_post` HTTP discipline extension (Theme ε) | Tek PR | 1 | — | ✅ PR #19 | | **8** | Webhook lifecycle vocabulary | Tek PR | 1 | — | ✅ PR #19 | -| **9** | Article 14: honesty fix + real staging + `forgelm approve` | Tek PR | 1 | Faz 6 (verify-audit benzeri pattern) | ✅ Wave 1 PR | -| **10** | Pydantic Literal sweep (6 kalan field) | Tek PR | 1 | — | ✅ Wave 1 PR | -| **11** | Wizard `_print` indirection + drop coverage omit | Tek PR | 1 | — | ✅ Wave 1 PR | -| **12** | Fixture consolidation (`tests/_helpers/`) | Tek PR | 1 | — | ✅ Wave 1 PR | -| **13** | `--data-audit` deprecation discipline | Tek PR | 1 | — | ✅ Wave 1 PR | -| **14** | `data_audit/` package split (multi-PR series) | 5 PR (D-1..D-5) | 5 | — | ✅ Wave 1 PR | -| **15** | `cli/` package split (multi-PR series) | 6 PR (C-1..C-6) | 6 | — | ✅ Wave 1 PR | -| **16** | Pydantic `description=` migration with CI guard (multi-PR) | 4 PR | 4 | Faz 10 (Literal'lar tamamen önce) | 🔄 Wave 1 IP | -| **17** | `--workers N` audit determinism | Tek PR | 1 | Faz 14 (data_audit/) + Faz 15 (cli/) | 🔄 Wave 1 IP | -| **18** | **Library API support — Analysis & design** | Tek PR (design dosyası) | 1 | Faz 14 + 15 | 🔄 Wave 1 IP | -| **19** | **Library API support — Implementation** | Tek PR | 1 | Faz 18 | 🔄 Wave 1 IP | -| **20** | **GDPR right-to-erasure — Analysis & design** | Tek PR | 1 | Faz 6 + 9 (audit + staging) | 🔄 Wave 1 IP | -| **21** | **GDPR right-to-erasure — Implementation** | Tek PR | 1 | Faz 20 | 🔄 Wave 1 IP | -| **22** | **ISO 27001 / SOC 2 Type II — Analysis & gap assessment** | Tek PR | 1 | Faz 3, 6, 7, 8, 9, 21 (security + compliance baseline) | 🔄 Wave 1 IP | -| **23** | **ISO 27001 / SOC 2 Type II — Implementation** | Tek PR (büyük) | 1 | Faz 22 | 🔄 Wave 1 IP | -| **24** | Bilingual TR mirror sweep (configuration-tr, distributed-tr, ingestion-tr, EN/TR user-manual H2 drift) | Tek PR | 1 | Faz 16 (description= migration ile config docs auto-gen) | 🔄 Wave 1 IP | -| **25** | Site picker honesty + site-as-tested-surface CI guard | Tek PR | 1 | Faz 1 (site honesty fixes önce) | ✅ Wave 1 PR | -| **26** | QMS bilingual EN+TR mirror + compliance docs cleanup | Tek PR | 1 | Faz 23 (ISO/SOC 2 dokümanları QMS ile lockstep) | 🔄 Wave 1 IP | -| **27** | Silent `except Exception:` sweep (Theme D residual) | Tek PR | 1 | — | ✅ Wave 1 PR | -| **28** | Remaining Major + Minor cleanup | Tek PR | 1 | Hepsi öncesindekilere bağlı | 🔄 Wave 1 IP | -| **29** | Nit sweep (40 madde mass-edit) | Tek PR | 1 | — | ✅ Wave 1 PR | -| **30** | **Final documentation + site finalization (EN+TR only)** — tüm `docs/` + `docs/usermanuals/{en,tr}/` + `site/*` v0.5.5 nihai durumuna senkron | Tek PR | 1 | **Faz 1-29 + 34-38** (tüm fonksiyonel iş bitmiş olmalı) | 🔄 Wave 1 IP | -| **31** | **Cross-OS release-tag matrix workflow** | Tek PR | 1 | — (release öncesi her şeyden) | ✅ Wave 1 PR | -| **32** | `.pre-commit-config.yaml` (opsiyonel — kullanıcı tercihi) | Tek PR | 1 | — | ✅ Wave 1 PR | -| **34** | **`forgelm doctor` env-check subcommand** (GH-001 — onboarding bloker) | Tek PR | 1 | Faz 15 (cli/ split) | 🔄 Wave 1 IP | -| **35** | **Air-gap pre-cache subcommands** (`cache-models` + `cache-tasks`) (GH-002, GH-003) | Tek PR | 1 | Faz 15 + Faz 34 (doctor `--offline` paydaş) | 🔄 Wave 1 IP | -| **36** | **Compliance verification toolbelt** (`safety-eval` standalone, `verify-annex-iv`, `verify-gguf`) (GH-004, GH-005, GH-009) | Tek PR | 1 | Faz 6 (verify-audit pattern), Faz 9 (compliance baseline) | 🔄 Wave 1 IP | -| **37** | **`forgelm approvals` listing subcommand** — Faz 9 follow-up (GH-007) | Tek PR | 1 | Faz 9 (`approve` + `reject` + staging directory) | 🔄 Wave 1 IP | -| **38** | **`forgelm reverse-pii` GDPR Article 15 subcommand** (GH-014) | Tek PR | 1 | Faz 21 (GDPR erasure baseline) | 🔄 Wave 1 IP | +| **9** | Article 14: honesty fix + real staging + `forgelm approve` | Tek PR | 1 | Faz 6 (verify-audit benzeri pattern) | ✅ DONE | +| **10** | Pydantic Literal sweep (6 kalan field) | Tek PR | 1 | — | ✅ DONE | +| **11** | Wizard `_print` indirection + drop coverage omit | Tek PR | 1 | — | ✅ DONE | +| **12** | Fixture consolidation (`tests/_helpers/`) | Tek PR | 1 | — | ✅ DONE | +| **13** | `--data-audit` deprecation discipline | Tek PR | 1 | — | ✅ DONE | +| **14** | `data_audit/` package split (multi-PR series) | 5 PR (D-1..D-5) | 5 | — | ✅ DONE | +| **15** | `cli/` package split (multi-PR series) | 6 PR (C-1..C-6) | 6 | — | ✅ DONE | +| **16** | Pydantic `description=` migration with CI guard (multi-PR) | 4 PR | 4 | Faz 10 (Literal'lar tamamen önce) | ⏳ PENDING | +| **17** | `--workers N` audit determinism | Tek PR | 1 | Faz 14 (data_audit/) + Faz 15 (cli/) | ⏳ PENDING | +| **18** | **Library API support — Analysis & design** | Tek PR (design dosyası) | 1 | Faz 14 + 15 | ⏳ PENDING | +| **19** | **Library API support — Implementation** | Tek PR | 1 | Faz 18 | ⏳ PENDING | +| **20** | **GDPR right-to-erasure — Analysis & design** | Tek PR | 1 | Faz 6 + 9 (audit + staging) | ⏳ PENDING | +| **21** | **GDPR right-to-erasure — Implementation** | Tek PR | 1 | Faz 20 | ⏳ PENDING | +| **22** | **ISO 27001 / SOC 2 Type II — Analysis & gap assessment** | Tek PR | 1 | Faz 3, 6, 7, 8, 9, 21 (security + compliance baseline) | ⏳ PENDING | +| **23** | **ISO 27001 / SOC 2 Type II — Implementation** | Tek PR (büyük) | 1 | Faz 22 | ⏳ PENDING | +| **24** | Bilingual TR mirror sweep (configuration-tr, distributed-tr, ingestion-tr, EN/TR user-manual H2 drift) | Tek PR | 1 | Faz 16 (description= migration ile config docs auto-gen) | ⏳ PENDING | +| **25** | Site picker honesty + site-as-tested-surface CI guard | Tek PR | 1 | Faz 1 (site honesty fixes önce) | ✅ DONE | +| **26** | QMS bilingual EN+TR mirror + compliance docs cleanup | Tek PR | 1 | Faz 23 (ISO/SOC 2 dokümanları QMS ile lockstep) | ⏳ PENDING | +| **27** | Silent `except Exception:` sweep (Theme D residual) | Tek PR | 1 | — | ✅ DONE | +| **28** | Remaining Major + Minor cleanup | Tek PR | 1 | Hepsi öncesindekilere bağlı | ⏳ PENDING | +| **29** | Nit sweep (40 madde mass-edit) | Tek PR | 1 | — | ✅ DONE | +| **30** | **Final documentation + site finalization (EN+TR only)** — tüm `docs/` + `docs/usermanuals/{en,tr}/` + `site/*` v0.5.5 nihai durumuna senkron | Tek PR | 1 | **Faz 1-29 + 34-38** (tüm fonksiyonel iş bitmiş olmalı) | ⏳ PENDING | +| **31** | **Cross-OS release-tag matrix workflow** | Tek PR | 1 | — (release öncesi her şeyden) | ✅ DONE | +| **32** | `.pre-commit-config.yaml` (opsiyonel — kullanıcı tercihi) | Tek PR | 1 | — | ✅ DONE | +| **34** | **`forgelm doctor` env-check subcommand** (GH-001 — onboarding bloker) | Tek PR | 1 | Faz 15 (cli/ split) | ⏳ PENDING | +| **35** | **Air-gap pre-cache subcommands** (`cache-models` + `cache-tasks`) (GH-002, GH-003) | Tek PR | 1 | Faz 15 + Faz 34 (doctor `--offline` paydaş) | ⏳ PENDING | +| **36** | **Compliance verification toolbelt** (`safety-eval` standalone, `verify-annex-iv`, `verify-gguf`) (GH-004, GH-005, GH-009) | Tek PR | 1 | Faz 6 (verify-audit pattern), Faz 9 (compliance baseline) | ⏳ PENDING | +| **37** | **`forgelm approvals` listing subcommand** — Faz 9 follow-up (GH-007) | Tek PR | 1 | Faz 9 (`approve` + `reject` + staging directory) | ⏳ PENDING | +| **38** | **`forgelm reverse-pii` GDPR Article 15 subcommand** (GH-014) | Tek PR | 1 | Faz 21 (GDPR erasure baseline) | ⏳ PENDING | | **33** | **v0.5.5 RELEASE** (CHANGELOG, version bump, tag, PyPI) | Tek PR | 1 | **Tüm önceki fazlar (1-32, 34-38)** | ⏳ POST-WAVE | **Toplam:** 38 faz, ~52 PR (multi-PR seri'ler dahil). **Status legend:** -- ✅ Wave 1 PR — kod yazıldı, Wave 1 PR'ında live (review/merge bekliyor) -- 🔄 Wave 1 IP — Wave 1 kapsamında, in progress veya başlanmamış -- ⏳ POST-WAVE — Wave 1 sonrasına kalır (release) +- ✅ DONE — Wave 1 PR merge edildi (`closure/wave1-faz9-32` → main, 2026-05-02); faz tamamlandı. +- ⏳ PENDING — henüz başlanmadı; bağımlılıkları sağlanan ilk batch sıradaki Wave 2'de açılır. +- ⏳ POST-WAVE — release fazı (33), tüm fonksiyonel iş bittikten sonra son. + +**Wave 1 kapanış özeti (post-merge):** 12 faz tamamlandı (Faz 9-15, 25, 27, 29, 31, 32). Foundation (Faz 1-8) Wave 0'da PR #19 ile merge edilmişti. Kalan **17 faz + 1 release** (Faz 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 34, 35, 36, 37, 38, 33) Wave 2+ kapsamında. --- @@ -332,7 +334,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. --- -### Faz 9 — Article 14: honesty fix + real staging directory + `forgelm approve` (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 9 — Article 14: honesty fix + real staging directory + `forgelm approve` (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-compliance-101 (Critical) — **PATH (a) + PATH (b)** birlikte @@ -394,7 +396,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. ## 5. Code hygiene fazları (Faz 10-13) -### Faz 10 — Pydantic Literal sweep (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 10 — Pydantic Literal sweep (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-101 (Major), F-compliance-105 (Major) @@ -416,7 +418,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. --- -### Faz 11 — Wizard `_print` indirection + drop coverage omit (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 11 — Wizard `_print` indirection + drop coverage omit (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-105 (Major), F-test-003 (Major), F-code-019 (Major carry-over) @@ -436,7 +438,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. --- -### Faz 12 — Fixture consolidation (`tests/_helpers/`) (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 12 — Fixture consolidation (`tests/_helpers/`) (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-test-004 (Major), F-test-005 (Major), F-code-015 (Major carry-over) @@ -459,7 +461,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. --- -### Faz 13 — `--data-audit` deprecation discipline (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 13 — `--data-audit` deprecation discipline (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-107 (Major), F-business-024 (Minor) @@ -485,7 +487,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. ## 6. Architectural splits (Faz 14-16) -### Faz 14 — `data_audit/` package split (5 PR series) ✅ TAMAMLANDI — Wave 1 PR +### Faz 14 — `data_audit/` package split (5 PR series) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-103 (Major) @@ -503,7 +505,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. --- -### Faz 15 — `cli/` package split (6 PR series) ✅ TAMAMLANDI — Wave 1 PR +### Faz 15 — `cli/` package split (6 PR series) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-104 (Major) @@ -826,7 +828,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 25 — Site picker honesty + site-as-tested-surface CI guard (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 25 — Site picker honesty + site-as-tested-surface CI guard (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-loc-001 (Major), F-loc-003 (Major), Theme α (site claim/code drift) @@ -887,7 +889,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 27 — Silent `except Exception:` sweep (Theme D residual) (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 27 — Silent `except Exception:` sweep (Theme D residual) (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-code-106 (Major) @@ -941,7 +943,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 29 — Nit sweep (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 29 — Nit sweep (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** Kalan 40 Nit (Faz 28'da bitmemiş olanlar). @@ -967,7 +969,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 34 — `forgelm doctor` env-check subcommand (Tek PR) 🔄 Wave 1 IN PROGRESS +### Faz 34 — `forgelm doctor` env-check subcommand (Tek PR) ⏳ PENDING **Closes:** GH-001 (Critical — onboarding bloker; 30 referans across 8 doc dosyası, hem EN hem TR mirror). @@ -1012,7 +1014,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 35 — Air-gap pre-cache subcommands (Tek PR) 🔄 Wave 1 IN PROGRESS +### Faz 35 — Air-gap pre-cache subcommands (Tek PR) ⏳ PENDING **Closes:** GH-002 (`cache-models`, High — air-gap workflow bloker), GH-003 (`cache-tasks`, High — air-gap workflow bloker). @@ -1056,7 +1058,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 36 — Compliance verification toolbelt (Tek PR) 🔄 Wave 1 IN PROGRESS +### Faz 36 — Compliance verification toolbelt (Tek PR) ⏳ PENDING **Closes:** GH-004 (`verify-annex-iv`, High — compliance bloker), GH-005 (`safety-eval` standalone, High — eval gap), GH-009 (`verify-gguf`, Medium — deployment integrity). @@ -1099,7 +1101,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 37 — `forgelm approvals` listing subcommand (Tek PR) 🔄 Wave 1 IN PROGRESS +### Faz 37 — `forgelm approvals` listing subcommand (Tek PR) ⏳ PENDING **Closes:** GH-007 (Medium — Faz 9'un eksik kalan listing parçası; operator pending approval'ları manuel filesystem inspection'la bulamamalı). @@ -1143,7 +1145,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m --- -### Faz 38 — `forgelm reverse-pii` GDPR Article 15 subcommand (Tek PR) 🔄 Wave 1 IN PROGRESS +### Faz 38 — `forgelm reverse-pii` GDPR Article 15 subcommand (Tek PR) ⏳ PENDING **Closes:** GH-014 (Medium — GDPR Article 15 right of access; data subject "benim verim eğitim setinde mi?" sorgusuna yanıt veren tooling). @@ -1370,7 +1372,7 @@ Her v0.5.5 yeni feature için aşağıdaki üçlü zorunlu — eksik olan varsa ## 11. Release prep + release fazları (Faz 31-33) -### Faz 31 — Cross-OS release-tag matrix workflow (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 31 — Cross-OS release-tag matrix workflow (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-test-007 (Major) — kullanıcı kararı: release-tag-only matrix. @@ -1467,7 +1469,7 @@ Her v0.5.5 yeni feature için aşağıdaki üçlü zorunlu — eksik olan varsa --- -### Faz 32 — `.pre-commit-config.yaml` (opsiyonel) (Tek PR) ✅ TAMAMLANDI — Wave 1 PR +### Faz 32 — `.pre-commit-config.yaml` (opsiyonel) (Tek PR) ✅ TAMAMLANDI (Wave 1 — merged) **Closes:** F-test-008 (Major) — kullanıcı kararı için: **A (opsiyonel) öneriliyor.** @@ -1932,6 +1934,7 @@ Wave 1 PR (closure/wave1-faz9-32) review fazında ek olarak **28 ghost entry** t Bu plan v2 (revised 2026-04-30 kullanıcı kararları sonrası). **v2.1 (2026-04-30):** §15.5 Foundation PR carry-over registry eklendi. +**v2.3 (2026-05-02 — post-merge):** Wave 1 PR (`closure/wave1-faz9-32`) main'e merge edildi. Status değişiklikleri: 12 faz `✅ Wave 1 PR` → `✅ DONE` (Faz 9, 10, 11, 12, 13, 14, 15, 25, 27, 29, 31, 32); 17 faz + Faz 30 + Faz 34-38 `🔄 Wave 1 IP` → `⏳ PENDING`. Wave 2 başlangıcı için 7 immediate-batch faz dependency-free durumda (§14 güncel haritada). **v2.2 (2026-05-02):** Ghost feature analysis absorption — Wave 1 PR review aşamasında tespit edilen 28 ghost entry plan'a entegre edildi. Yeni Faz 34-38 açıldı (`forgelm doctor`, `cache-models` + `cache-tasks`, compliance verification toolbelt, `approvals` listing, `reverse-pii`); Faz 30 Task O ghost feature naming/flag drift sweep eklendi (10 doc-only Tier 1 fix); Phase 13 + Phase 14 deferred row'ları Faz 30 Task A'ya eklendi; Faz 21 GH-023 absorption ile `ingestion.retention` extend edildi. Toplam faz sayısı 33 → 38; toplam ~52 PR. Wave 1 IN PROGRESS işaretleri Faz 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 34, 35, 36, 37, 38 satırlarına uygulandı. Plan dosyası: `docs/analysis/code_reviews/closure-plan-202604300906.md`. Her PR description'ında bu plana referans verir (`§N` faz numarası ile). From 305e1a4c20dc2475416cd939a80eb4513e5b0549 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sat, 2 May 2026 15:42:13 +0300 Subject: [PATCH 2/3] fix(audit): close Wave 1 post-merge audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1 audit raporundaki açık iş'ler kapatıldı. Plan §11 Code 5/5 + Faz 7 trust_remote_code disiplini + Faz 10 risk taxonomy artık tamamen temiz. CODE — Faz 7 acceptance gate (5 from_pretrained sites): - synthetic.py:260,261 — explicit trust_remote_code=False on local teacher model load (synthetic-data path). - judge.py:166,167 — explicit trust_remote_code=False on local judge model (auto-revert gate consults this). - export.py:151,152 — explicit trust_remote_code=False on the GGUF export merge step. model.py / inference.py / merging.py / fit_check.py already pin via ``model_kwargs["trust_remote_code"]`` or explicit kwargs; the audit's literal grep was a false positive there. CODE — Faz 10 risk taxonomy expansion: - config.py: ``RiskAssessmentConfig.risk_category`` and ``ComplianceMetadataConfig.risk_classification`` Literals widened from the 3-tier shipped set (high-risk/limited-risk/minimal-risk) to the 5-tier EU AI Act set (unknown/minimal-risk/limited-risk/high-risk/ unacceptable). Default stays "minimal-risk" so existing configs validate unchanged. ``unacceptable`` covers Article 5 prohibited practices, ``unknown`` is the explicit unclassified placeholder. - test_config_literal_sweep.py: TestRiskClassificationLiteral updated to cover all 5 values + default-stable test; new TestRiskCategoryLiteral mirror. +9 tests (1001 → 1010). CODE — Faz 27 wider except Exception cluster (~22 sites): The original Faz 27 commit narrowed safety/trainer/judge/compliance/ _streaming, but ~22 ``except Exception as e:`` sites in deploy / benchmark / synthetic / export / chat / ingestion / merging / quickstart / utils / fit_check / inference / model + 2 cli dispatchers were left without BLE001 carve-out justification. Each site now carries a context-aware comment explaining the broad failure surface it covers (subprocess + filesystem + torch + tokenizer + network + HF Hub + lm-eval + pypdf-internal + ...) and why a narrow class is infeasible. Two sites (benchmark.py JSON write + quickstart.py audit event write + export.py model_integrity) narrowed to (OSError, TypeError, ValueError) instead of BLE001 since their surface is genuinely small. DOCS — drift fixes: - docs/roadmap.md:7: header "Tür" → "Type" (Faz 1 quick-win that the original carry-over registry missed). - closure-plan-202604300906.md Faz 25 acceptance: noted the e57aaf8 site-picker revert to 6 languages so the plan no longer contradicts ``site/js/i18n.js:27``. - closure-plan Faz 27 acceptance: added the wider-grep gate (``except Exception as `` form) so the next audit cannot find the same hole. - closure-plan Faz 15: acknowledgement note that forgelm/cli/_parser.py 662 lines is deliberate and exceeds the 600-line cohesion ceiling on purpose (single-source argparse registrar dosyası). - closure-plan §16 versioning: v2.4 entry summarising this round. Tests: 1001 → 1010 passed (+9 risk-Literal tests). ruff format/check clean. Coverage 67.93% (unchanged within rounding). Co-Authored-By: Claude Sonnet 4.6 --- .../code_reviews/closure-plan-202604300906.md | 12 ++++-- docs/roadmap.md | 2 +- forgelm/benchmark.py | 11 ++++-- forgelm/chat.py | 4 +- forgelm/cli/_training.py | 2 +- forgelm/cli/subcommands/_chat.py | 2 +- forgelm/config.py | 15 ++++++- forgelm/deploy.py | 2 +- forgelm/export.py | 20 +++++++--- forgelm/fit_check.py | 4 +- forgelm/inference.py | 2 +- forgelm/ingestion.py | 6 +-- forgelm/judge.py | 8 +++- forgelm/merging.py | 2 +- forgelm/model.py | 2 +- forgelm/quickstart.py | 9 ++++- forgelm/synthetic.py | 9 ++++- forgelm/utils.py | 2 +- tests/test_config_literal_sweep.py | 39 ++++++++++++++++++- 19 files changed, 116 insertions(+), 37 deletions(-) diff --git a/docs/analysis/code_reviews/closure-plan-202604300906.md b/docs/analysis/code_reviews/closure-plan-202604300906.md index e271612c..1db03147 100644 --- a/docs/analysis/code_reviews/closure-plan-202604300906.md +++ b/docs/analysis/code_reviews/closure-plan-202604300906.md @@ -2,7 +2,7 @@ **Plan ID:** `closure-plan-202604300906` **Created:** 2026-04-30 -**Last revised:** 2026-05-02 (v2.3 — Wave 1 PR merge edildi; ✅ Wave 1 PR → ✅ DONE, 🔄 Wave 1 IN PROGRESS → ⏳ PENDING, kalan iş Wave 2+) +**Last revised:** 2026-05-02 (v2.4 — post-merge audit closure: 4 kod fix + 2 plan doc update; tüm Wave 1 fazları gerçekten temiz) **Branch:** `main` **Commit (baseline):** `6b515ed912f8f22304194c1b3f55ed07a26f519c` **Source review:** [`master-review-opus-202604300906.md`](./master-review-opus-202604300906.md) @@ -406,7 +406,7 @@ Diğer her şeyden bağımsız. Paralel açılabilir. - `DistributedConfig.fsdp_backward_prefetch` → `Literal["backward_pre", "backward_post"]` - `DistributedConfig.fsdp_state_dict_type` → `Literal["FULL_STATE_DICT", "SHARDED_STATE_DICT"]` - `SafetyConfig.scoring` → `Literal["binary", "confidence_weighted"]` - - `ComplianceMetadataConfig.risk_classification` → `Literal["unknown", "minimal", "limited", "high-risk", "unacceptable"]` + - `ComplianceMetadataConfig.risk_classification` → `Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"]` *(post-merge audit closure: kod ilk merge'de 3-değer set ile shipped (`high-risk`/`limited-risk`/`minimal-risk`); plan'ın spec ettiği 5-değer set'e genişletildi — `unknown` ve `unacceptable` eklendi, `-risk` suffix kod-uyumlu korundu. `RiskAssessmentConfig.risk_category` aynı set'le lockstep. Default `"minimal-risk"` korunduğu için mevcut config'ler backward-compatible.)* - `GaLoreConfig.galore_optim` ve `galore_proj_type` → gerçek seçenekleri grep'le bul, Literal'e çevir. 2. Bespoke runtime validators sil (Pydantic Literal otomatik validate eder). 3. `config_template.yaml` `--dry-run` yeşil olduğunu doğrula. @@ -523,6 +523,8 @@ Diğer her şeyden bağımsız. Paralel açılabilir. **Note:** Faz 14 ve Faz 15 PR'ları paralel-safe pair'lar halinde (D-1||C-1, D-2||C-2, D-4||C-3, D-5||C-4) açılabilir. +**Post-merge note:** `forgelm/cli/_parser.py` 662 satır — plan §11 Code 5/5 kriterindeki 600-satır limit'i 62 satır aşıyor. Karar: kabul (deliberate). `_parser.py` tek-amaçlı bir dosya: **tüm** subcommand registrar'larını ve `parse_args`'ı host'lar (9 subcommand × ~50-100 satır argparse boilerplate + shared `_add_common_subparser_flags`). Sub-split önerileri ya (a) ortak flag set'lerini parçalar (DRY ihlali) ya da (b) dispatcher → registrar yönü tersine döndürür (import topology bozulur). Limit'i bu dosya için 700'e çıkarmak Faz 30 standards revision'da değerlendirilir. + --- ### Faz 16 — Pydantic `description=` migration with CI guard (4 PR series) @@ -856,10 +858,10 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m 10. `site/js/guide.js:436, 448, 455, 458, 466, 654` — `.toLowerCase()` → `.toLocaleLowerCase(state.lang)` (Türkçe İ/I bozulmasın). **Faz 25 Acceptance:** -- Site picker sadece EN+TR. +- ~~Site picker sadece EN+TR.~~ **OVERTURNED (commit `e57aaf8`):** Picker 6 dile geri çekildi — orijinal trim "wrong premise" gerekçesiyle revert edildi. `site/js/i18n.js:27` `SUPPORTED = ['en', 'tr', 'de', 'fr', 'es', 'zh']`. Yeni policy: site-level translations (i18n.js) 6 dil, user-manual content EN+TR-only + sparse-emit (deferred languages için manual EN-fallback). `docs/standards/localization.md:8-13` ve commit `e57aaf8` mesajı authoritative kaynaklar. - `tools/check_site_claims.py --strict` exit 0; bir testte `compliance.py`'da artefact filename değiştir → CI fail. - Tüm site sayfaları doğru `og:locale` + `hreflang`. -- Standards explicit deferred policy. +- Standards explicit deferred policy (yukarıdaki revert sonrası reformülize edildi). --- @@ -906,6 +908,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m **Faz 27 Acceptance:** - `grep -rn "except Exception:" forgelm/ | grep -v "# noqa: BLE001"` boş. +- **Genişletilmiş gate (post-merge audit closure):** `grep -rn "except Exception as " forgelm/ | grep -v "# noqa: BLE001" | grep -v "# pragma: no cover"` de boş — başlangıçta yalnızca `except Exception:` colon-form'unu yakalayan grep, `except Exception as e:` form'undaki ~22 site'ı kaçırmıştı. Tümü context-aware BLE001 + justification ile kapatıldı (deploy/benchmark/synthetic/export/chat/ingestion/merging/quickstart/utils/fit_check/inference/model + cli dispatchers). - error-handling.md best-effort carve-out clear. - `_expert_index_in_name` regex-based, 3 architecture fixture testi yeşil. @@ -1934,6 +1937,7 @@ Wave 1 PR (closure/wave1-faz9-32) review fazında ek olarak **28 ghost entry** t Bu plan v2 (revised 2026-04-30 kullanıcı kararları sonrası). **v2.1 (2026-04-30):** §15.5 Foundation PR carry-over registry eklendi. +**v2.4 (2026-05-02 — post-merge audit closure):** Wave 1 post-merge audit raporundaki açık iş'ler kapatıldı. Kod tarafı: (a) Faz 7 — synthetic.py / judge.py / export.py'da 6 `from_pretrained` site'a explicit `trust_remote_code=False` eklendi; (b) Faz 10 — `risk_classification` + `risk_category` Literal'ları 3-değer'den 5-değer EU AI Act tier set'ine genişletildi (default unchanged; backward compatible) + 8 yeni test; (c) Faz 27 — `except Exception as e:` form'undaki ~22 site context-aware BLE001 + justification ile kapatıldı, gate sıkılaştırıldı; (d) Faz 1 — `docs/roadmap.md` "Tür" → "Type" header drift fix. Doc tarafı: Faz 25 acceptance'a "site picker reverted to 6 languages (commit e57aaf8)" notu, Faz 15 acceptance'a `_parser.py` 662-satır kabul gerekçesi. **v2.3 (2026-05-02 — post-merge):** Wave 1 PR (`closure/wave1-faz9-32`) main'e merge edildi. Status değişiklikleri: 12 faz `✅ Wave 1 PR` → `✅ DONE` (Faz 9, 10, 11, 12, 13, 14, 15, 25, 27, 29, 31, 32); 17 faz + Faz 30 + Faz 34-38 `🔄 Wave 1 IP` → `⏳ PENDING`. Wave 2 başlangıcı için 7 immediate-batch faz dependency-free durumda (§14 güncel haritada). **v2.2 (2026-05-02):** Ghost feature analysis absorption — Wave 1 PR review aşamasında tespit edilen 28 ghost entry plan'a entegre edildi. Yeni Faz 34-38 açıldı (`forgelm doctor`, `cache-models` + `cache-tasks`, compliance verification toolbelt, `approvals` listing, `reverse-pii`); Faz 30 Task O ghost feature naming/flag drift sweep eklendi (10 doc-only Tier 1 fix); Phase 13 + Phase 14 deferred row'ları Faz 30 Task A'ya eklendi; Faz 21 GH-023 absorption ile `ingestion.retention` extend edildi. Toplam faz sayısı 33 → 38; toplam ~52 PR. Wave 1 IN PROGRESS işaretleri Faz 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 34, 35, 36, 37, 38 satırlarına uygulandı. diff --git a/docs/roadmap.md b/docs/roadmap.md index da907ee2..0af9dfa2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ ## Status at a glance -| Tür | Phase | Status | +| Type | Phase | Status | |-----|-------|--------| | ✅ Done | [Phase 1-9](roadmap/completed-phases.md) | SOTA upgrades, evaluation, reliability, enterprise integration, ecosystem, alignment stack, safety, EU AI Act compliance (Articles 9-17 + Annex IV), advanced safety intelligence | | ✅ Done | [Phase 10 — Post-Training Completion](roadmap/phase-10-post-training.md) | `inference.py`, `chat`, `export` (GGUF), `--fit-check`, `deploy` — shipped `v0.4.0` | diff --git a/forgelm/benchmark.py b/forgelm/benchmark.py index 7c82d6f0..32b2085c 100644 --- a/forgelm/benchmark.py +++ b/forgelm/benchmark.py @@ -93,7 +93,12 @@ def _save_benchmark_json( indent=2, ) logger.info("Benchmark results saved to %s", results_path) - except Exception as e: + except (OSError, TypeError, ValueError) as e: + # OSError: filesystem (ENOSPC, permission, broken parent dir). + # TypeError/ValueError: ``json.dump`` rejecting an unserialisable + # object inside the lm-eval result tree. Saving the artefact is + # non-fatal: the run already completed and metrics live in the + # returned BenchmarkResult. logger.warning("Failed to save benchmark results: %s", e) @@ -135,7 +140,7 @@ def run_benchmark( try: lm_obj = HFLM(pretrained=model, tokenizer=tokenizer, batch_size=batch_size) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: lm-eval HFLM wrapper construction crosses HF model introspection (AttributeError on architecture mismatch), tokenizer compatibility (ValueError), CUDA init (RuntimeError), and lm-eval-internal config parsing; surfacing as BenchmarkResult(passed=False) is the documented hard-failure surface so the trainer auto-revert gate can react. # NOSONAR logger.error("Failed to initialize lm-eval model wrapper: %s", e) return BenchmarkResult(passed=False, failure_reason=f"Model wrapper initialization failed: {e}") @@ -145,7 +150,7 @@ def run_benchmark( try: results = lm_eval.simple_evaluate(model=lm_obj, tasks=tasks, limit=limit, **task_kwargs) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: lm-eval simple_evaluate runs a wide task surface (dataset download OSError, task spec ValueError, model.generate RuntimeError on CUDA OOM/dtype mismatch, lm-eval-internal AssertionError); BenchmarkResult(passed=False) is the documented hard-failure surface for the auto-revert gate. # NOSONAR logger.error("Benchmark evaluation failed: %s", e) return BenchmarkResult(passed=False, failure_reason=f"Evaluation execution failed: {e}") diff --git a/forgelm/chat.py b/forgelm/chat.py index ccaadf64..ea3ee0af 100644 --- a/forgelm/chat.py +++ b/forgelm/chat.py @@ -271,7 +271,7 @@ def _generate_and_print(self, user_input: str) -> None: ): self._print_inline_raw(token) response += token - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: streaming generate crosses CUDA OOM (RuntimeError), tokenizer issues (ValueError), and stop-token race conditions; the chat REPL must keep running after a single failed turn so the operator can retry / change parameters. # NOSONAR self._print(f"\n[Generation error: {e}]") return self._print("") # trailing newline @@ -285,7 +285,7 @@ def _generate_and_print(self, user_input: str) -> None: temperature=self.temperature, max_new_tokens=self.max_new_tokens, ) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: see streaming branch above; same broad torch / tokenizer surface, same per-turn soft-fail policy in the REPL loop. # NOSONAR self._print(f"[Generation error: {e}]") return # Non-streaming path: full response is also untrusted model output diff --git a/forgelm/cli/_training.py b/forgelm/cli/_training.py index c9fdcfdf..53e66265 100644 --- a/forgelm/cli/_training.py +++ b/forgelm/cli/_training.py @@ -70,7 +70,7 @@ def _run_training_pipeline(config, args, json_output: bool) -> None: sys.exit(EXIT_AWAITING_APPROVAL) sys.exit(EXIT_SUCCESS if result.success else EXIT_EVAL_FAILURE) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: top-of-CLI training catch. ForgeTrainer.train() crosses every concern (HF model load, dataset load, TRL trainer, safety eval, judge eval, audit emission, compliance export, webhook notify); any leak from the inner narrow-class catches must surface as a structured CLI failure (with traceback in the log) rather than a Python traceback dumped to stdout that breaks JSON parsers. # NOSONAR _report_training_error( json_output, payload={"success": False, "error": str(e)}, diff --git a/forgelm/cli/subcommands/_chat.py b/forgelm/cli/subcommands/_chat.py index e4c7b8f0..65a36397 100644 --- a/forgelm/cli/subcommands/_chat.py +++ b/forgelm/cli/subcommands/_chat.py @@ -29,6 +29,6 @@ def _run_chat_cmd(args) -> None: trust_remote_code=args.trust_remote_code, backend=args.backend, ) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: top-of-subcommand catch. ``run_chat`` loads the model + tokenizer, optionally a PEFT adapter, and runs the REPL loop; failure surface includes OSError (model path), RuntimeError (CUDA), ValueError (tokenizer), KeyboardInterrupt swap, plus chat-internal generation errors. EXIT_TRAINING_ERROR is the documented public contract for a failed dispatched subcommand. # NOSONAR logger.exception("Chat session failed: %s", e) sys.exit(EXIT_TRAINING_ERROR) diff --git a/forgelm/config.py b/forgelm/config.py index a2963af8..dc8caa32 100644 --- a/forgelm/config.py +++ b/forgelm/config.py @@ -317,7 +317,13 @@ class RiskAssessmentConfig(BaseModel): intended_use: str = "" foreseeable_misuse: List[str] = [] - risk_category: Literal["high-risk", "limited-risk", "minimal-risk"] = "minimal-risk" + # EU AI Act risk taxonomy. ``unacceptable`` covers Article 5 prohibited + # practices; ``high-risk`` covers Article 6 systems requiring full Annex + # IV documentation; ``limited-risk`` and ``minimal-risk`` cover the + # transparency-only and unrestricted tiers; ``unknown`` is the explicit + # placeholder for systems that have not yet been classified. The default + # stays ``"minimal-risk"`` so existing configs validate unchanged. + risk_category: Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"] = "minimal-risk" mitigation_measures: List[str] = [] vulnerable_groups_considered: bool = False @@ -346,7 +352,12 @@ class ComplianceMetadataConfig(BaseModel): intended_purpose: str = "" known_limitations: str = "" system_version: str = "" - risk_classification: Literal["high-risk", "limited-risk", "minimal-risk"] = "minimal-risk" + # See ``RiskAssessmentConfig.risk_category`` for the taxonomy rationale; + # the two Literal sets are kept in lockstep so the canonical EU AI Act + # tiers remain a single source of truth. + risk_classification: Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"] = ( + "minimal-risk" + ) class SyntheticConfig(BaseModel): diff --git a/forgelm/deploy.py b/forgelm/deploy.py index 16e294e3..5b9819fe 100644 --- a/forgelm/deploy.py +++ b/forgelm/deploy.py @@ -297,6 +297,6 @@ def generate_deploy_config( logger.info("Deploy config written to %s", output_path) return DeployResult(success=True, target=target, output_path=output_path, content=content) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: deploy-config generators cross filesystem writes (OSError), template rendering (KeyError/ValueError), and target-specific YAML/JSON serialisation; surfacing every variant as a structured DeployResult is the documented public contract. # NOSONAR logger.error("Failed to generate deploy config for target '%s': %s", target, e) return DeployResult(success=False, target=target, error=str(e)) diff --git a/forgelm/export.py b/forgelm/export.py index 33a0f623..fba7d526 100644 --- a/forgelm/export.py +++ b/forgelm/export.py @@ -148,8 +148,14 @@ def _merge_adapter(model_path: str, adapter_path: str, merged_dir: str) -> None: from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Merging adapter %s into base model %s ...", adapter_path, model_path) - tokenizer = AutoTokenizer.from_pretrained(model_path) - model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.float16, device_map="cpu") + # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): + # the merge step is part of the GGUF export pipeline; loading the base + # model must not execute repo-bundled code. Operators with a custom + # architecture should fork and pre-convert before exporting. + tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=False) + model = AutoModelForCausalLM.from_pretrained( + model_path, torch_dtype=torch.float16, device_map="cpu", trust_remote_code=False + ) model = PeftModel.from_pretrained(model, adapter_path) model = model.merge_and_unload() @@ -203,7 +209,11 @@ def _update_integrity_manifest(model_dir: str, export_result: ExportResult) -> N json.dump(data, f, indent=2) logger.info("model_integrity.json updated with exported artifact.") - except Exception as e: + except (OSError, ValueError, json.JSONDecodeError) as e: + # OSError: filesystem write / read failure on integrity_path. + # ValueError / JSONDecodeError: corrupt or partial existing manifest. + # Updating the manifest is non-fatal: the artefact itself was already + # exported successfully and its on-disk SHA-256 is recoverable. logger.debug("Could not update model_integrity.json: %s", e) @@ -299,7 +309,7 @@ def _run_converter(cmd: List[str], fmt: str, actual_quant: str) -> Optional[Expo quant=actual_quant, error="GGUF conversion timed out after 3600 seconds.", ) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: subprocess.run for the GGUF converter (llama.cpp / convert-hf-to-gguf.py) crosses OSError (binary missing), CalledProcessError (non-zero exit before check=False catches it), and the converter's own deep-stack errors; ExportResult(success=False) is the documented public contract for the export pipeline. # NOSONAR return ExportResult(success=False, format=fmt, quant=actual_quant, error=str(e)) if proc.returncode == 0: @@ -385,7 +395,7 @@ def export_model( try: _merge_adapter(model_path, adapter, merged_dir) source_path = merged_dir - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: _merge_adapter loads HF base + PEFT adapter and writes the merged dir; failure surface includes OSError (disk/path), RuntimeError (CUDA/dtype), KeyError (missing adapter config), and HF/PEFT-internal errors. ExportResult(success=False) is the documented hard-failure contract. # NOSONAR return ExportResult(success=False, format=fmt, quant=quant, error=f"Adapter merge failed: {e}") actual_quant, actual_output_path = _resolve_kquant_path(quant, output_path) diff --git a/forgelm/fit_check.py b/forgelm/fit_check.py index e3891412..89d54212 100644 --- a/forgelm/fit_check.py +++ b/forgelm/fit_check.py @@ -88,7 +88,7 @@ def _load_arch_params(model_name_or_path: str, trust_remote_code: bool = False) params["num_attention_heads"] = getattr(cfg, "num_attention_heads", None) params["num_key_value_heads"] = getattr(cfg, "num_key_value_heads", None) logger.debug("Loaded architecture config from %s", model_name_or_path) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: AutoConfig.from_pretrained surfaces OSError (network/cache miss), HF repo errors, ValueError on unknown architecture, and ImportError when the optional config-class module is missing. fit-check fallback uses regex name-hints (3b/7b/13b/...) so a config probe failure does not abort the VRAM estimate. # NOSONAR logger.debug("Could not load AutoConfig for %s: %s — using size hint fallback.", model_name_or_path, e) # Fill in missing values using size-hint lookup on the model name. @@ -299,7 +299,7 @@ def _detect_available_vram_gb(torch_module: Any) -> tuple[Optional[float], bool] free_bytes, _ = torch_module.cuda.mem_get_info() return free_bytes / (1024**3), False return None, True - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: ``torch.cuda.mem_get_info`` surfaces RuntimeError (driver / CUDA init), AttributeError (older torch / no CUDA), and OSError (corrupt driver state). Returning ``(None, True)`` falls back to a hypothetical-only fit verdict. # NOSONAR logger.debug("Could not query GPU memory: %s", e) return None, True diff --git a/forgelm/inference.py b/forgelm/inference.py index fe33c30a..08647a78 100644 --- a/forgelm/inference.py +++ b/forgelm/inference.py @@ -289,7 +289,7 @@ def _gen_thread() -> None: try: with torch.inference_mode(): model.generate(**gen_kwargs) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: streaming generate runs in a worker thread; the broad catch funnels every torch error (CUDA OOM RuntimeError, dtype mismatch, tokenizer ValueError, KeyboardInterrupt swap) into the shared list so the foreground thread can re-raise after joining. # NOSONAR _exc.append(e) thread = Thread(target=_gen_thread, daemon=True) diff --git a/forgelm/ingestion.py b/forgelm/ingestion.py index 1c1aac3d..a8cbc436 100644 --- a/forgelm/ingestion.py +++ b/forgelm/ingestion.py @@ -227,7 +227,7 @@ def _read_pdf_pages(reader: Any, path: Path) -> List[str]: for idx, page in enumerate(reader.pages): try: text = page.extract_text() or "" - except Exception as exc: + except Exception as exc: # noqa: BLE001 — best-effort: pypdf's per-page extraction surface is wide (KeyError on malformed object refs, AssertionError on broken cross-ref tables, UnicodeDecodeError on font encodings, plus its own internal errors); per-page soft-fail keeps the run going so a single bad page cannot abort a multi-thousand-document corpus ingest. # NOSONAR logger.warning( "PDF page extraction failed (file=%s, page_index=%d): %s — page skipped, run continues.", path, @@ -257,7 +257,7 @@ def _extract_pdf(path: Path, *, dedup_state: Optional[Dict[str, int]] = None) -> try: reader = PdfReader(str(path)) - except Exception as exc: + except Exception as exc: # noqa: BLE001 — best-effort: PdfReader open surfaces OSError (file/permission), pypdf-internal errors (bad header, unsupported version, malformed xref), and on rare adversarial inputs InfiniteLoopError; converting all to a typed ValueError keeps the per-file error path uniform with DOCX/EPUB extractors. # NOSONAR raise ValueError(f"Could not open PDF '{path}': {exc}") from exc if getattr(reader, "is_encrypted", False): @@ -1055,7 +1055,7 @@ def _extract_text_for_ingest( return extractor(fpath) except ImportError: raise - except Exception as exc: + except Exception as exc: # noqa: BLE001 — best-effort: per-file dispatcher catch — each format-specific extractor (PDF/DOCX/EPUB/TXT/MD) raises its own typed ValueError above + a wide tail of corpus-data-driven failures; per-file soft-fail with skip-and-continue keeps a multi-thousand-file corpus ingest running. ImportError stays narrow above so missing-extra failures still propagate. # NOSONAR logger.warning("Skipping '%s' (extraction failed): %s", fpath, exc) return None diff --git a/forgelm/judge.py b/forgelm/judge.py index aeeecdd7..f9e79469 100644 --- a/forgelm/judge.py +++ b/forgelm/judge.py @@ -163,8 +163,12 @@ def _load_local_judge(judge_model: str) -> Tuple[Any, Any]: from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Loading local judge model: %s", judge_model) - tok = AutoTokenizer.from_pretrained(judge_model) - mdl = AutoModelForCausalLM.from_pretrained(judge_model, device_map="auto") + # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): a + # judge model is consulted by the auto-revert gate, so loading must not + # execute arbitrary repo code at load time. Operators with a custom + # architecture should fork and pre-convert. + tok = AutoTokenizer.from_pretrained(judge_model, trust_remote_code=False) + mdl = AutoModelForCausalLM.from_pretrained(judge_model, device_map="auto", trust_remote_code=False) return mdl, tok diff --git a/forgelm/merging.py b/forgelm/merging.py index 5b4f86ad..aba4c03f 100644 --- a/forgelm/merging.py +++ b/forgelm/merging.py @@ -79,7 +79,7 @@ def merge_peft_adapters( num_models=len(adapters), ) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: model merging crosses HF model load (OSError/RuntimeError), PEFT adapter load (KeyError on missing config), torch tensor ops (RuntimeError on dtype/device mismatch), and mergekit-internal errors. MergeResult(success=False) is the documented public contract. # NOSONAR logger.error("Model merging failed: %s", e) return MergeResult(success=False, error=str(e)) diff --git a/forgelm/model.py b/forgelm/model.py index cbb9eb97..47bda3b5 100644 --- a/forgelm/model.py +++ b/forgelm/model.py @@ -225,7 +225,7 @@ def _recast_expert_weight(name: str, module, target_dtype) -> bool: return False try: module.weight.data = module.weight.data.to(target_dtype) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: per-expert weight recast runs across hundreds of MoE expert tensors; surface includes RuntimeError (dtype unsupported on device), AttributeError (frozen / shared parameter), and torch internal errors on edge architectures. Returning False keeps the per-expert loop running so a single recast failure cannot abort the whole sweep. # NOSONAR logger.debug("Could not optimize %s: %s", name, e) return False return True diff --git a/forgelm/quickstart.py b/forgelm/quickstart.py index 864231be..f9f35fce 100644 --- a/forgelm/quickstart.py +++ b/forgelm/quickstart.py @@ -231,7 +231,7 @@ def _detect_available_vram_gb() -> Optional[float]: # Use total (capacity) rather than free (current snapshot) for the # "will this template fit at all" question. return max_total_bytes / (1024**3) - except Exception as exc: # pragma: no cover — best-effort only + except Exception as exc: # noqa: BLE001 — best-effort: VRAM probe runs ``nvidia-smi`` parsing + torch.cuda introspection + pynvml fallback; surface includes FileNotFoundError (no nvidia-smi), RuntimeError (no CUDA), AttributeError (older torch), and parser errors. Returning None falls back to a conservative VRAM estimate. # pragma: no cover — best-effort only. # NOSONAR logger.debug("VRAM probe failed: %s", exc) return None @@ -366,7 +366,12 @@ def _append_audit_event(audit_dir: Path, event: Dict[str, Any]) -> None: log_path = audit_dir / "quickstart_audit.jsonl" with open(log_path, "a", encoding="utf-8") as f: f.write(json.dumps(entry, default=str) + "\n") - except Exception as exc: + except (OSError, TypeError, ValueError) as exc: + # OSError: filesystem write (ENOSPC, permission, broken parent dir). + # TypeError / ValueError: ``json.dumps`` rejecting an unserialisable + # object even with ``default=str`` (e.g. a circular reference). + # The quickstart audit log is supplementary; the primary audit + # chain in compliance.AuditLogger remains authoritative. logger.warning("Failed to write quickstart audit event: %s", exc) diff --git a/forgelm/synthetic.py b/forgelm/synthetic.py index 98518335..28b43b60 100644 --- a/forgelm/synthetic.py +++ b/forgelm/synthetic.py @@ -62,7 +62,7 @@ def _generate_one(self, prompt: str, idx: int, result: SyntheticResult) -> Optio """ try: response = self._call_teacher(prompt) - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: per-prompt teacher invocation crosses three backends (api / local model / pre-recorded file) each with their own failure tail (network HTTPError, RuntimeError on CUDA OOM, KeyError on missing seed entry); per-prompt soft-fail with structured error capture is the documented contract so a single bad prompt cannot abort a corpus-scale run. # NOSONAR result.failed += 1 result.errors.append(f"Prompt {idx}: {e}") logger.warning("Generation failed for prompt %d: %s", idx, e) @@ -257,11 +257,16 @@ def _load_local_teacher(self): from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Loading local teacher model: %s", self.synth_cfg.teacher_model) - tokenizer = AutoTokenizer.from_pretrained(self.synth_cfg.teacher_model) + # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): + # synthetic-data generation must never execute repo-bundled code from + # an arbitrary teacher checkpoint. Operators that genuinely need a + # custom architecture should fork and pre-convert. + tokenizer = AutoTokenizer.from_pretrained(self.synth_cfg.teacher_model, trust_remote_code=False) model = AutoModelForCausalLM.from_pretrained( self.synth_cfg.teacher_model, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, device_map="auto" if torch.cuda.is_available() else None, + trust_remote_code=False, ) self._teacher = (model, tokenizer) diff --git a/forgelm/utils.py b/forgelm/utils.py index 30615603..eee8fb5e 100644 --- a/forgelm/utils.py +++ b/forgelm/utils.py @@ -40,7 +40,7 @@ def setup_authentication(token: Optional[str] = None) -> None: try: login(token=hf_token) logger.info("Hugging Face authentication successful.") - except Exception as e: + except Exception as e: # noqa: BLE001 — best-effort: huggingface_hub login surfaces network errors (ConnectionError/Timeout), API errors (HTTPError, repo-permission), config errors (LocalEntryNotFoundError), and credential errors (ValueError on malformed token); auth failure is non-fatal so a public-models-only run can still proceed. # NOSONAR logger.warning( "Hugging Face authentication failed: %s. Private models and gated datasets may not be accessible.", e, diff --git a/tests/test_config_literal_sweep.py b/tests/test_config_literal_sweep.py index b62debd8..d011c3ea 100644 --- a/tests/test_config_literal_sweep.py +++ b/tests/test_config_literal_sweep.py @@ -62,14 +62,49 @@ def test_invalid_scoring_raises(self): class TestRiskClassificationLiteral: - @pytest.mark.parametrize("value", ["high-risk", "limited-risk", "minimal-risk"]) + @pytest.mark.parametrize( + "value", + ["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"], + ) def test_valid_classification(self, value): c = ComplianceMetadataConfig(risk_classification=value) assert c.risk_classification == value def test_invalid_classification_raises(self): with pytest.raises(ValidationError, match="risk_classification"): - ComplianceMetadataConfig(risk_classification="unknown") + ComplianceMetadataConfig(risk_classification="not-a-risk-tier") + + def test_minimal_risk_default(self): + # Default stays "minimal-risk" so existing configs validate unchanged + # after the value-set extension (Faz 10 closure: 3 → 5 EU AI Act + # tiers covering Article 5 prohibited + unknown/unclassified). + c = ComplianceMetadataConfig() + assert c.risk_classification == "minimal-risk" + + +class TestRiskCategoryLiteral: + """``RiskAssessmentConfig.risk_category`` mirrors ``risk_classification``. + + The two fields are deliberately kept in lockstep (single source of truth + for the EU AI Act tier list); changing one without the other would + silently drift the validation of the two halves of a compliance config. + """ + + @pytest.mark.parametrize( + "value", + ["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"], + ) + def test_valid_category(self, value): + from forgelm.config import RiskAssessmentConfig + + r = RiskAssessmentConfig(risk_category=value) + assert r.risk_category == value + + def test_invalid_category_raises(self): + from forgelm.config import RiskAssessmentConfig + + with pytest.raises(ValidationError, match="risk_category"): + RiskAssessmentConfig(risk_category="not-a-risk-tier") class TestGaloreOptimLiteral: From 7abf9a374dd863a8f00b31ef752c033228258653 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sat, 2 May 2026 16:34:01 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(review):=20PR=20#22=20bot=20comments=20?= =?UTF-8?q?=E2=80=94=206=20findings=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit flagged the most important one: the new ``unacceptable`` risk tier was reachable via Pydantic but not propagated to the runtime compliance gate or the wizard prompt. Closed alongside five smaller review-bot suggestions. CRITICAL — runtime-gate propagation (CodeRabbit): - config.py: ``_warn_high_risk_compliance`` now matches a shared ``_STRICT_RISK_TIERS = frozenset({"high-risk", "unacceptable"})`` set instead of a literal ``"high-risk"`` string, so ``unacceptable`` gets at least the same auto_revert nudge. A dedicated second warning fires when the classification is ``unacceptable`` to flag EU AI Act Article 5 prohibited-practices status. - wizard.py: interactive risk-classification prompt extended from the 3-tier (minimal/limited/high) menu to the full 5-tier RiskTier set so the wizard never offers a value the schema would reject and never hides a value the schema accepts. - test_eu_ai_act.py: ``test_high_risk_warnings`` updated for the new warning text; new ``test_unacceptable_risk_warnings`` covers the Article 5 dedicated notice; new ``test_minimal_risk_does_not_warn`` pins the negative case so a future taxonomy edit cannot silently start nagging minimal-risk operators. DRY (Sourcery): - config.py: introduced ``RiskTier = Literal[...]`` type alias and rewrote ``RiskAssessmentConfig.risk_category`` and ``ComplianceMetadataConfig.risk_classification`` to reference it. Also exposed ``_STRICT_RISK_TIERS`` as the single source of truth for the high-risk/unacceptable gate. Test coverage (Sourcery): - test_config_literal_sweep.py: added ``TestRiskCategoryLiteral.test_minimal_risk_default`` mirroring the existing ``TestRiskClassificationLiteral.test_minimal_risk_default`` so the two fields' defaults can never silently diverge. Narrow exception class (Gemini): - export.py:212-219: ``except (OSError, ValueError, json.JSONDecodeError)`` → ``except (OSError, TypeError, ValueError)``. ``JSONDecodeError`` is a ``ValueError`` subclass so the previous form was redundant; adding ``TypeError`` matches the json.dump unserialisable-object surface and keeps this site consistent with the other two narrow-class sites (benchmark.py:96, quickstart.py:369). English-only (Qodo): - export.py / judge.py / synthetic.py / test_config_literal_sweep.py: ``Faz N`` → ``Phase N`` in code comments + the test module docstring. CLAUDE.md mandates English in code artefacts; the closure plan stays Turkish (it's a project-internal tracking doc, not source). Style (Sourcery): - closure-plan §11 Faz 25 acceptance: Turkish grammar nit — "authoritative kaynaklar." → "authoritative kaynaklardır." for a cleaner sentence ending. Tests: 1010 → 1013 passed (+3 new strict-gate scenario tests). ruff format/check clean. Coverage 67.95% (up 0.02 from 67.93). Co-Authored-By: Claude Sonnet 4.6 --- .../code_reviews/closure-plan-202604300906.md | 2 +- forgelm/config.py | 67 +++++++++++++------ forgelm/export.py | 8 ++- forgelm/judge.py | 2 +- forgelm/synthetic.py | 2 +- forgelm/wizard.py | 7 +- tests/test_config_literal_sweep.py | 13 +++- tests/test_eu_ai_act.py | 38 ++++++++++- 8 files changed, 109 insertions(+), 30 deletions(-) diff --git a/docs/analysis/code_reviews/closure-plan-202604300906.md b/docs/analysis/code_reviews/closure-plan-202604300906.md index 1db03147..90bb6eac 100644 --- a/docs/analysis/code_reviews/closure-plan-202604300906.md +++ b/docs/analysis/code_reviews/closure-plan-202604300906.md @@ -858,7 +858,7 @@ Yeni dosya: `docs/analysis/code_reviews/iso27001-soc2-alignment-202604YYMMHHMM.m 10. `site/js/guide.js:436, 448, 455, 458, 466, 654` — `.toLowerCase()` → `.toLocaleLowerCase(state.lang)` (Türkçe İ/I bozulmasın). **Faz 25 Acceptance:** -- ~~Site picker sadece EN+TR.~~ **OVERTURNED (commit `e57aaf8`):** Picker 6 dile geri çekildi — orijinal trim "wrong premise" gerekçesiyle revert edildi. `site/js/i18n.js:27` `SUPPORTED = ['en', 'tr', 'de', 'fr', 'es', 'zh']`. Yeni policy: site-level translations (i18n.js) 6 dil, user-manual content EN+TR-only + sparse-emit (deferred languages için manual EN-fallback). `docs/standards/localization.md:8-13` ve commit `e57aaf8` mesajı authoritative kaynaklar. +- ~~Site picker sadece EN+TR.~~ **OVERTURNED (commit `e57aaf8`):** Picker 6 dile geri çekildi — orijinal trim "wrong premise" gerekçesiyle revert edildi. `site/js/i18n.js:27` `SUPPORTED = ['en', 'tr', 'de', 'fr', 'es', 'zh']`. Yeni policy: site-level translations (i18n.js) 6 dil, user-manual content EN+TR-only + sparse-emit (deferred languages için manual EN-fallback). `docs/standards/localization.md:8-13` ve commit `e57aaf8` mesajı authoritative kaynaklardır. - `tools/check_site_claims.py --strict` exit 0; bir testte `compliance.py`'da artefact filename değiştir → CI fail. - Tüm site sayfaları doğru `og:locale` + `hreflang`. - Standards explicit deferred policy (yukarıdaki revert sonrası reformülize edildi). diff --git a/forgelm/config.py b/forgelm/config.py index dc8caa32..64a223c4 100644 --- a/forgelm/config.py +++ b/forgelm/config.py @@ -310,6 +310,24 @@ class EvaluationConfig(BaseModel): ) +# EU AI Act risk taxonomy — single source of truth shared by +# ``RiskAssessmentConfig.risk_category`` and +# ``ComplianceMetadataConfig.risk_classification`` so the two Pydantic fields +# can never drift. ``unacceptable`` covers Article 5 prohibited practices; +# ``high-risk`` covers Article 6 systems requiring full Annex IV documentation; +# ``limited-risk`` and ``minimal-risk`` cover the transparency-only and +# unrestricted tiers; ``unknown`` is the explicit placeholder for systems that +# have not yet been classified. The default for both fields stays +# ``"minimal-risk"`` so existing configs validate unchanged. +RiskTier = Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"] + +# Tiers that demand full Annex IV documentation + auto-revert + safety gates +# under the EU AI Act. Keep this set in lockstep with +# ``ForgeConfig._warn_high_risk_compliance`` and the wizard prompt so the new +# tier is reachable + enforced everywhere the old high-risk-only set was. +_STRICT_RISK_TIERS: frozenset[str] = frozenset({"high-risk", "unacceptable"}) + + class RiskAssessmentConfig(BaseModel): """Art. 9: Risk management — declare risks before training.""" @@ -317,13 +335,7 @@ class RiskAssessmentConfig(BaseModel): intended_use: str = "" foreseeable_misuse: List[str] = [] - # EU AI Act risk taxonomy. ``unacceptable`` covers Article 5 prohibited - # practices; ``high-risk`` covers Article 6 systems requiring full Annex - # IV documentation; ``limited-risk`` and ``minimal-risk`` cover the - # transparency-only and unrestricted tiers; ``unknown`` is the explicit - # placeholder for systems that have not yet been classified. The default - # stays ``"minimal-risk"`` so existing configs validate unchanged. - risk_category: Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"] = "minimal-risk" + risk_category: RiskTier = "minimal-risk" mitigation_measures: List[str] = [] vulnerable_groups_considered: bool = False @@ -352,12 +364,7 @@ class ComplianceMetadataConfig(BaseModel): intended_purpose: str = "" known_limitations: str = "" system_version: str = "" - # See ``RiskAssessmentConfig.risk_category`` for the taxonomy rationale; - # the two Literal sets are kept in lockstep so the canonical EU AI Act - # tiers remain a single source of truth. - risk_classification: Literal["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"] = ( - "minimal-risk" - ) + risk_classification: RiskTier = "minimal-risk" class SyntheticConfig(BaseModel): @@ -489,16 +496,38 @@ def _warn_general_consistency(self) -> None: ) def _warn_high_risk_compliance(self) -> None: - """EU AI Act high-risk compliance recommendations.""" - is_high_risk = (self.risk_assessment and self.risk_assessment.risk_category == "high-risk") or ( - self.compliance and self.compliance.risk_classification == "high-risk" + """EU AI Act compliance recommendations for strict risk tiers. + + ``unacceptable`` (Article 5 prohibited practices) is treated at least + as strictly as ``high-risk`` for ForgeLM's purposes — the gate exists + to nudge the operator into running with auto-revert + safety eval, + and ``unacceptable`` should never get *less* gating than ``high-risk`` + because the underlying use case is not allowed at all under the Act. + """ + is_strict = (self.risk_assessment and self.risk_assessment.risk_category in _STRICT_RISK_TIERS) or ( + self.compliance and self.compliance.risk_classification in _STRICT_RISK_TIERS ) - if not is_high_risk: + if not is_strict: return if not self.evaluation or not self.evaluation.auto_revert: logger.warning( - "High-risk AI classification requires evaluation.auto_revert: true " - "for EU AI Act compliance. Safety gates should be enabled." + "Risk classification %r requires evaluation.auto_revert: true " + "for EU AI Act compliance. Safety gates should be enabled.", + # Use whichever field is set; both share the same RiskTier. + (self.compliance and self.compliance.risk_classification) + or (self.risk_assessment and self.risk_assessment.risk_category), + ) + # Article 5 prohibited practices warrant a louder operator notice on + # top of the auto_revert nudge — the deployment itself is unlawful in + # the EU regardless of how well the safety gates are wired up. + unacceptable = (self.risk_assessment and self.risk_assessment.risk_category == "unacceptable") or ( + self.compliance and self.compliance.risk_classification == "unacceptable" + ) + if unacceptable: + logger.warning( + "Risk classification 'unacceptable' corresponds to EU AI Act Article 5 prohibited " + "practices. ForgeLM will not refuse the run, but deploying such a system inside the " + "EU is unlawful — confirm operator intent before continuing." ) safety = self.evaluation.safety if self.evaluation else None if not safety or not safety.enabled: diff --git a/forgelm/export.py b/forgelm/export.py index fba7d526..fedd358f 100644 --- a/forgelm/export.py +++ b/forgelm/export.py @@ -148,7 +148,7 @@ def _merge_adapter(model_path: str, adapter_path: str, merged_dir: str) -> None: from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Merging adapter %s into base model %s ...", adapter_path, model_path) - # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): + # ``trust_remote_code=False`` is the secure default (Phase 7 acceptance): # the merge step is part of the GGUF export pipeline; loading the base # model must not execute repo-bundled code. Operators with a custom # architecture should fork and pre-convert before exporting. @@ -209,9 +209,11 @@ def _update_integrity_manifest(model_dir: str, export_result: ExportResult) -> N json.dump(data, f, indent=2) logger.info("model_integrity.json updated with exported artifact.") - except (OSError, ValueError, json.JSONDecodeError) as e: + except (OSError, TypeError, ValueError) as e: # OSError: filesystem write / read failure on integrity_path. - # ValueError / JSONDecodeError: corrupt or partial existing manifest. + # TypeError: ``json.dump`` rejecting an unserialisable artefact field. + # ValueError: corrupt or partial existing manifest (covers + # ``json.JSONDecodeError`` since it subclasses ``ValueError``). # Updating the manifest is non-fatal: the artefact itself was already # exported successfully and its on-disk SHA-256 is recoverable. logger.debug("Could not update model_integrity.json: %s", e) diff --git a/forgelm/judge.py b/forgelm/judge.py index f9e79469..7dbe252f 100644 --- a/forgelm/judge.py +++ b/forgelm/judge.py @@ -163,7 +163,7 @@ def _load_local_judge(judge_model: str) -> Tuple[Any, Any]: from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Loading local judge model: %s", judge_model) - # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): a + # ``trust_remote_code=False`` is the secure default (Phase 7 acceptance): a # judge model is consulted by the auto-revert gate, so loading must not # execute arbitrary repo code at load time. Operators with a custom # architecture should fork and pre-convert. diff --git a/forgelm/synthetic.py b/forgelm/synthetic.py index 28b43b60..775d6b36 100644 --- a/forgelm/synthetic.py +++ b/forgelm/synthetic.py @@ -257,7 +257,7 @@ def _load_local_teacher(self): from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Loading local teacher model: %s", self.synth_cfg.teacher_model) - # ``trust_remote_code=False`` is the secure default (Faz 7 acceptance): + # ``trust_remote_code=False`` is the secure default (Phase 7 acceptance): # synthetic-data generation must never execute repo-bundled code from # an arbitrary teacher checkpoint. Operators that genuinely need a # custom architecture should fork and pre-convert. diff --git a/forgelm/wizard.py b/forgelm/wizard.py index 5136319a..c559554d 100644 --- a/forgelm/wizard.py +++ b/forgelm/wizard.py @@ -218,9 +218,12 @@ def _collect_compliance_config() -> Optional[dict]: "provider_name": _prompt("Organization name"), "intended_purpose": _prompt("Intended purpose of the model"), "risk_classification": _prompt_choice( + # Five-tier set kept in lockstep with ``forgelm.config.RiskTier`` + # so the wizard never offers a value the Pydantic schema would + # reject (and never hides a value the schema accepts). "Risk classification:", - ["minimal-risk", "limited-risk", "high-risk"], - default=1, + ["unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable"], + default=1, # ``minimal-risk`` — same default as the Pydantic field. ), } diff --git a/tests/test_config_literal_sweep.py b/tests/test_config_literal_sweep.py index d011c3ea..9999fac9 100644 --- a/tests/test_config_literal_sweep.py +++ b/tests/test_config_literal_sweep.py @@ -1,4 +1,4 @@ -"""Faz 10: parse-time Literal validation sweep. +"""Phase 10: parse-time Literal validation sweep. Each enum-shaped string field that was tightened from `str` to `Literal[...]` must accept every documented value and raise `pydantic.ValidationError` @@ -76,7 +76,7 @@ def test_invalid_classification_raises(self): def test_minimal_risk_default(self): # Default stays "minimal-risk" so existing configs validate unchanged - # after the value-set extension (Faz 10 closure: 3 → 5 EU AI Act + # after the value-set extension (Phase 10 closure: 3 → 5 EU AI Act # tiers covering Article 5 prohibited + unknown/unclassified). c = ComplianceMetadataConfig() assert c.risk_classification == "minimal-risk" @@ -106,6 +106,15 @@ def test_invalid_category_raises(self): with pytest.raises(ValidationError, match="risk_category"): RiskAssessmentConfig(risk_category="not-a-risk-tier") + def test_minimal_risk_default(self): + # Mirror of TestRiskClassificationLiteral.test_minimal_risk_default — + # the two fields share the RiskTier alias and must therefore share + # the default; surfaces any future divergence between them. + from forgelm.config import RiskAssessmentConfig + + r = RiskAssessmentConfig() + assert r.risk_category == "minimal-risk" + class TestGaloreOptimLiteral: @pytest.mark.parametrize( diff --git a/tests/test_eu_ai_act.py b/tests/test_eu_ai_act.py index 05f8d93d..874e82f2 100644 --- a/tests/test_eu_ai_act.py +++ b/tests/test_eu_ai_act.py @@ -123,7 +123,43 @@ def test_high_risk_warnings(self, caplog, minimal_config): risk_assessment={"risk_category": "high-risk"}, ) ) - assert "High-risk AI" in caplog.text + assert "high-risk" in caplog.text + assert "auto_revert" in caplog.text + + def test_unacceptable_risk_warnings(self, caplog, minimal_config): + """``unacceptable`` (Article 5) must trip the strict gate AND emit + the dedicated prohibited-practices warning on top of the auto_revert + nudge. Closes the runtime-propagation gap CodeRabbit flagged after + the 3 → 5 RiskTier expansion.""" + import logging + + with caplog.at_level(logging.WARNING, logger="forgelm.config"): + ForgeConfig( + **minimal_config( + risk_assessment={"risk_category": "unacceptable"}, + ) + ) + # Strict gate fires: same auto_revert nudge as high-risk. + assert "unacceptable" in caplog.text + assert "auto_revert" in caplog.text + # Dedicated Article 5 prohibited-practices notice fires too. + assert "Article 5" in caplog.text + assert "prohibited" in caplog.text + + def test_minimal_risk_does_not_warn(self, caplog, minimal_config): + """``minimal-risk`` and ``limited-risk`` must NOT trip the strict + gate — keeps the friction off operators running unrestricted / + transparency-tier systems.""" + import logging + + with caplog.at_level(logging.WARNING, logger="forgelm.config"): + ForgeConfig( + **minimal_config( + risk_assessment={"risk_category": "minimal-risk"}, + ) + ) + assert "auto_revert" not in caplog.text + assert "Article 5" not in caplog.text def test_yaml_round_trip(self, tmp_path, minimal_config): data = minimal_config(