Skip to content

feat: Phase 10.5 Quickstart Layer (v0.4.5) - #9

Merged
cemililik merged 41 commits into
mainfrom
development
Apr 26, 2026
Merged

cemililik merged 41 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Apr 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • One-command bundled templates — forgelm quickstart customer-support renders a config the existing trainer accepts unchanged, copies a small seed dataset, runs training, and drops into chat with the result.
  • 5 templates under forgelm/templates/ (all QLoRA 4-bit, rank=8, batch=1 — designed to fit on a 12 GB consumer GPU): customer-support, code-assistant, domain-expert (BYOD), medical-qa-tr (Turkish medical Q&A with safety disclaimers), grpo-math.
  • GPU-aware auto-downsizing — picks primary 7B model when ≥10 GB VRAM available, fallback ≤2B otherwise; explicit no-gpu-detected reason without CUDA.
  • Wizard integration — forgelm --wizard now opens with "Start from a template?" Yes routes to quickstart; No falls through to the existing 8-step interactive flow. Same code paths, same YAML schema — no bifurcation.
  • CI smoke test — nightly runs quickstart --dry-run + --config <out> --dry-run for every bundled template; catches template drift the moment a config schema changes.
  • pip install forgelm users get templates too via [tool.setuptools.package-data].

Closes Phase 10.5 of the roadmap. Bumps version to 0.4.5.

What changed

Area File(s) Note
New module forgelm/quickstart.py Template registry (frozen dataclass), auto_select_model(), run_quickstart() orchestrator
New assets forgelm/templates/ (5 dirs + LICENSES.md) Configs, seed JSONLs, attribution table (CC-BY-SA 4.0, author-original)
CLI forgelm/cli.py New quickstart subcommand: --list/--model/--dataset/--output/--dry-run/--no-chat
Wizard forgelm/wizard.py "Start from a template?" entry point routes to quickstart or falls through to existing flow
Tests tests/test_quickstart.py (40 tests) Includes regression test that loads every generated YAML through load_config() — strongest guard against template drift
CI .github/workflows/nightly.yml Per-template smoke test
Build pyproject.toml Version 0.4.5; package-data bundles templates into the wheel
Docs README.md, CHANGELOG.md, docs/guides/quickstart.md, docs/roadmap.md/-tr.md, docs/roadmap/phase-12-quickstart.md, docs/roadmap/releases.md Phase 10.5 marked Done

Test plan

  • ruff format . && ruff check . — clean
  • pytest tests/test_quickstart.py — 40/40 pass
  • Full suite: 519 passed, 1 skipped (3 pre-existing TRL/torch FSDP failures in test_grpo_reward.py unrelated to this PR — verified via git stash)
  • forgelm quickstart --list works
  • forgelm quickstart customer-support --dry-run writes YAML and exits 0
  • Generated YAML loads cleanly via forgelm.config.load_config
  • Reviewer: spot-check one bundled JSONL for license-clean / well-formed entries
  • Reviewer: confirm pip install -e . then forgelm quickstart --list works in a fresh venv (templates bundle correctly)

Notes for reviewer

  • The 17 commits visible in this PR include unsquashed Phase 10 history that was already squash-merged to main via PR feat: Phase 10 post-training completion (v0.4.0) #7. The actual code diff (above) is clean and Phase 10.5-only — recommend squash merge to keep main history tidy.
  • No breaking changes; all new surface is additive.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • One‑command quickstart: bundled templates, GPU‑aware model selection/auto‑downsizing, list/JSON/dry‑run, model/dataset/output overrides, optional post‑run chat, audit logging, and wizard integration.
  • Bug Fixes

    • Chat save ensures output directories exist; stricter dataset/BYOD validation; GRPO max‑length accepts legacy key alias.
  • Tests

    • Large expansion of unit, integration, packaging, and nightly wheel/install smoke tests.
  • Packaging

    • Templates included in the wheel; project version bumped to v0.4.5.

cemililik and others added 18 commits April 25, 2026 13:33
Closes the "trained, now what?" gap with five new modules:

- forgelm/inference.py: shared generation primitives (load_model, generate,
  generate_stream, logit_stats, adaptive_sample) used by chat, judge, synthetic
- forgelm/chat.py: interactive REPL with streaming, /reset /save /temperature
  /system slash commands, rich optional rendering, optional Llama Guard routing
- forgelm/fit_check.py: VRAM pre-flight advisor — architecture via AutoConfig,
  formula-based estimation, FITS/TIGHT/OOM/UNKNOWN verdict + recommendations
- forgelm/export.py: GGUF export wrapping llama-cpp-python converter; adapter
  merge; 6 quant levels; SHA-256 appended to model_integrity.json
- forgelm/deploy.py: deployment config generation for ollama/vllm/tgi/hf-endpoints

CLI: forgelm chat / export / deploy subcommands + --fit-check flag; full backward
compat with existing flat CLI. All subcommands work without --config.

Tests: 89 new tests across 5 modules; full suite 427 passed, 34 skipped.
Docs: CHANGELOG, README, roadmap, quickstart, usage reference updated.
Version: 0.3.1rc1 → 0.4.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- forgelm/chat.py: remove unused `rich.text.Text` import
- forgelm/inference.py: remove unused `math` import (auto-fixed)
- tests/test_safety_advanced.py: remove unused `result` assignment (F841)
- tests/test_cli_phase10.py: remove unused _run_deploy_cmd/_run_export_cmd imports
- tests/test_deploy.py: remove unused pytest import, sort imports (I001)
- tests/test_export.py: remove unused subprocess/pytest imports, sort imports
- tests/test_fit_check.py: remove unused types import
- tests/test_inference.py: sort import block (I001), remove unused importlib/math
- tests/test_oom_recovery.py: remove unused generate_training_manifest import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_oom_recovery: rename capture_manifest arg cfg→config to match
  generate_training_manifest(config=...) keyword call signature
- test_safety_advanced: patch transformers.pipeline instead of
  forgelm.safety.pipeline — pipeline is a local import inside
  run_safety_evaluation so no module-level attribute exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- roadmap.md: remove redundant "Phase 12 (reserved)" row; fix Phase 10.5
  Mermaid color (was green/done, still planned → blue); update doc map
  labels to reflect completed vs planned status
- completed-phases.md: add Phase 10 to summary table; replace 650-line
  Phase 10-13 duplicate content with a concise ✅ entry for Phase 10;
  remove Phase 11-13 content (each has its own dedicated file)
- phase-12-quickstart.md: update heading from "Phase 12" to "Phase 10.5";
  add file-name note explaining why filename stays as phase-12-*
- phase-11-data-ingestion.md: fix delivery target from "v0.4.5 or v0.5.0"
  to v0.5.0 (consistent with roadmap.md and releases.md)
- releases.md: remove duplicate --- separator; clarify v0.3.1rc1 status
  as folded into v0.4.0 rather than a standalone release candidate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat: emit warning instead of silent no-op in _safety_hint
- cli: add --output-format/--quiet/--log-level to chat/export/deploy
  subparsers using argparse.SUPPRESS so they don't override main parser
  values; remove redundant inner KeyboardInterrupt handler in chat
- deploy: parameterize cloud vendor for HF Endpoints config; fix vLLM
  comment generation to drop trailing colon for None values
- export: produce f16 GGUF for K-quants with explicit warning (two-step
  llama-quantize required); add FORGELM_GGUF_CONVERTER env var override;
  add subprocess timeout (3600s); cleanup merged adapter dir in finally;
  remove duplicate llama_cpp import
- fit_check: use free_bytes (not total) for available_gb; drop unused
  arch parameter from _build_recommendations
- inference: propagate exceptions from background generate thread; raise
  ValueError when adapter passed to Unsloth backend; warn when Unsloth
  receives load_in_4bit=False; switch 8-bit loading to BitsAndBytesConfig
- README: add 5 Phase 10 modules to project structure
- completed-phases: update scope note to Phase 1-10
- tests: fix tautological assertion in test_export; update test_fit_check
  to expect free_bytes; mock signatures accept timeout kwarg
- pyproject: clarify export extra comment re: K-quant two-step flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat: emit safety warning once per session in __init__ instead of every
  turn; remove unused _safety_hint method; route all streaming output and
  welcome banner through self._print/_print_inline so injected output_fn
  captures everything
- inference: forward load_in_4bit and load_in_8bit to _load_unsloth
  (signature now accepts both as keyword args) and pass them to
  FastLanguageModel.from_pretrained; warn on conflicting flags
- deploy: drop deprecated top-level "version" key from generated
  docker-compose.yaml
- fit_check: discard unused total_bytes via underscore unpack; simplify
  tautological 4-bit recommendation check; compute adapter parameter
  count as an integer directly to avoid float round-trip via GB
- README: update stale test count (145+/11 files → 427+/30 files)
- tests: simplify chat KI test (drop redundant MagicMock indirection);
  assert chat --help output documents model_path; replace manual cwd
  management in deploy test with monkeypatch.chdir; split tautological
  assertion in fit_check hypothetical-mode test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical:
- export: K-quant requests now write to a .f16.gguf intermediate path and
  ExportResult.quant reports "f16" (the actual file) instead of the
  requested K-quant. model_integrity.json SHA-256 stays honest — audit
  chain no longer claims a hash that doesn't match the file. Required
  llama-quantize follow-up step is logged with the exact path.
- inference: TextIteratorStreamer now has timeout=60 and the streaming
  loop catches queue.Empty. If model.generate() raises before the queue
  is populated (OOM, dtype mismatch, invalid input ids), the captured
  worker exception is re-raised instead of blocking the chat REPL forever.

High:
- chat: remove unimplemented --safety flag (was a no-op after round 2;
  CLAUDE.md "stub tagged production" anti-pattern). Welcome banner no
  longer claims "Safety: on".
- chat: token output now goes through _print_inline_raw which rich-escapes
  model tokens before rendering ([red]ALERT[/red] in training data was a
  low-grade prompt-injection vector). Rich console buffer is force-flushed
  so streaming UX doesn't appear frozen.
- fit_check: size-hint matching uses word-bounded regex so "llama-13b" no
  longer falls back to the 3B architecture profile in --offline mode.
- deploy: tgi/ollama targets now reject non-local model paths up front;
  HF Hub IDs no longer silently produce broken docker volumes / Modelfiles
  the user only discovers at deploy time.

Medium:
- export: adapter merge forces torch_dtype=float16 (CPU bf16 falls back to
  fp32 and stalls on large models); full converter stderr/stdout logged
  separately from the 500-char ExportResult.error so debug context is
  preserved.
- inference: fallback prompt template (no chat_template) now appends
  "assistant: " to mimic add_generation_prompt=True; raw base models stop
  echoing "user: ..." continuations.
- chat: /save persists the system prompt as the first JSONL message so
  saved transcripts are replayable.
- fit_check: breakdown includes estimation_caveat noting ±30% typical
  error and unmodeled factors (FlashAttention, MoE, sliding window).

Low:
- cli: comment explaining the outer KeyboardInterrupt guard around the
  chat dispatch covers Ctrl-C during model load / banner render.
- CHANGELOG: document the K-quant two-step requirement under v0.4.0.

Tests:
- test_kquant_produces_f16_intermediate: K-quant request → result.quant=f16
  and .f16.gguf path on disk.
- test_tgi_rejects_non_local_path / test_ollama_rejects_non_local_path:
  HF Hub IDs return success=False with an actionable error.
- test_export mocks now read --outfile from the cmd args so the new
  K-quant rerouting is tested transparently. Existing ollama/tgi tests
  create real model directories under tmp_path.

430 passed, 34 skipped (3 new tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes ~94 of 104 SonarCloud findings. Behavior is unchanged; remaining
critical findings are cognitive-complexity refactors that need their
own focused PRs.

Wave 1 — quick wins
-------------------

Security:
- nightly.yml / publish.yml: drop the workflow-level write permission and
  declare read-only at workflow scope; only the jobs that need write
  (notify-failure: issues; publish-testpypi/pypi: id-token) keep it.

Unused parameters:
- compliance.export_compliance_artifacts: drop the unused `config` arg.
  Manifest already has the config-derived data; updates 5 call sites
  (cli.py, trainer.py, test_compliance.py, test_integration_smoke.py).
- model._apply_moe_expert_quantization: drop unused `num_experts`.
- trainer.ForgeTrainer: drop unused `final_path` from
  _run_benchmark_if_configured / _run_safety_if_configured /
  _run_judge_if_configured / _export_compliance_if_needed; drop the
  unused `metrics` from _run_benchmark_if_configured. Updates the 4
  call sites in train().

Constructor → literal:
- model._apply_lora: lora_kwargs uses a dict literal.
- trainer._get_common_training_kwargs / _build_trainer: kwargs and
  trainer_kwargs use dict literals.
- inference.generate / generate_stream: gen_kwargs uses dict literals
  (separately flagged by SonarCloud).

Duplicate literals → constants:
- trainer: extract _EVT_REVERT_TRIGGERED for the audit event name used
  three times around the auto-revert paths.
- wizard: extract _PREFERENCE_COLUMNS_HINT for the dpo/simpo/orpo
  dataset-format hint.

PyTorch dim= explicit:
- merging._slerp_merge: switch to torch.linalg.vector_norm so SLERP's
  scalar-magnitude semantics are explicit (previously torch.norm with
  no dim).
- merging._ties_merge_tensor: pass dim=None to torch.quantile.

Misc:
- merging: rename unused `missing` to `_` in load_state_dict unpack.
- wizard.run_wizard: return type now Optional[str] (it already returned
  None in the "defer" branch).
- wizard: replace nested ternary for strategy_str with if/elif/else.
- config.DistributedConfig: rephrase inline comment to avoid
  string-literals-that-look-like-code.
- compliance._sanitize_md: type hint now Optional[str] (the body
  already handled None).

Wave 2 — float == pytest.approx
-------------------------------

Mechanical migration of ~60 assertions across 13 test files. These were
exact comparisons against literal config defaults / dataclass fields
(no FP arithmetic) so behavior is unchanged, but pytest.approx silences
SonarCloud's float-equality rule and is the conventional pattern.

Files: test_alignment, test_benchmark, test_compliance, test_config,
test_cost_estimation, test_galore, test_integration_smoke,
test_judge_functions, test_long_context, test_safety_advanced,
test_synthetic, test_trainer, test_webhook. Three files needed
`import pytest` added (test_compliance, test_long_context, test_webhook).

False positives left as-is:
- runtime_smoke.py:88 — already re-raises on non-zero exit; SystemExit(0)
  is the documented success path.
- test_safety_advanced.py:235 — `result =` was already removed in round-2.
- notebooks/kto_binary_feedback.ipynb — low-priority duplicate literal.

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the SonarCloud cognitive-complexity findings (≥15 → all <15)
without changing behavior. Each function was decomposed into private
helpers with single responsibilities; 430 tests still pass with no
regressions.

Small (CC 16–24):
- webhook._send → _resolve_url + _post_payload + _mask
- synthetic.generate → _generate_one + _write_output
- synthetic._call_file_teacher → _load_file_responses
- model._apply_moe_expert_quantization → _is_frozen_expert_weight +
  _recast_expert_weight
- model._freeze_unselected_experts → _parse_selected_experts +
  _expert_index_in_name
- judge.run_judge_evaluation → _load_eval_prompts + _load_local_judge +
  _generate_response + _clip_judge_score + _save_judge_results
- trainer._collect_resource_usage → _collect_gpu_info +
  _train_runtime_seconds + _resolve_cost_per_hour

Medium (CC 29–32):
- benchmark.run_benchmark → _extract_task_score + _parse_results +
  _save_benchmark_json (plus _ACC_METRIC_KEYS constant)
- model_card.generate_model_card → _build_metrics_table +
  _build_benchmark_section + _build_safety_section + _format_method +
  _build_extra_tags
- model.get_model_and_tokenizer → _load_unsloth + _load_tokenizer +
  _device_map_for + _build_model_kwargs + _apply_moe_config +
  _build_lora_config

Large (CC 39–66):
- cli._run_dry_run → _galore_dry_run_fields + _evaluation_dry_run_fields
  + _compliance_dry_run_fields + _build_dry_run_result
- config._validate_consistency → _warn_general_consistency +
  _warn_high_risk_compliance + _validate_trainer_type +
  _validate_galore + _validate_distributed
- cli.main → _dispatch_subcommand + _maybe_run_wizard +
  _load_config_or_exit + _apply_offline_flag + _maybe_run_no_train_mode
  + _run_training_pipeline
- trainer.train → _measure_baseline_loss + _apply_benchmark_result +
  _apply_resource_usage + _apply_safety_result + _apply_judge_result +
  _finalize_artifacts + _handle_human_approval_gate +
  _run_training_pipeline (the orchestrator stays a thin try/except)
- wizard.run_wizard → _collect_rope_scaling + _collect_neftune_alpha +
  _collect_webhook_config + _collect_safety_config +
  _collect_evaluation_config + _collect_compliance_config +
  _save_config_to_file + _print_wizard_summary
- data._make_batch_processor → _process_text_format +
  _process_messages_format + _format_user_assistant_row +
  _process_user_assistant_format
- data.prepare_dataset → _apply_mix_ratio + _merge_extra_datasets +
  _ensure_validation_split + _validate_trainer_columns +
  _shuffle_and_passthrough + _passthrough_for_trainer +
  _passthrough_multimodal + _format_sft_dataset
- safety.run_safety_evaluation → _load_safety_prompts +
  _generate_safety_responses + _release_model_from_gpu +
  _classify_one_response + _classify_responses +
  _evaluate_safety_gates + _save_safety_results

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verified findings:
- CHANGELOG: "afterwards" → "afterward" (American English consistency)
- CHANGELOG: v0.3.1rc1 release date corrected to 2026-03-28 (matches the
  actual `bump version to 0.3.1rc1` commit 7f827c0); v0.4.0 gains a
  Breaking section documenting that export_compliance_artifacts dropped
  its unused `config` parameter
- README: stale test-count line updated to "430 passed (+34 skipped)
  across 30 test files"
- fit_check: regex lookbehind now blocks `.` so size-hint matching
  doesn't classify "1.7b" as the 7B architecture (also caught Llama-3.2
  fractional sizes)
- fit_check._build_recommendations: drop unused `quant` param + matching
  call site (body uses `m.load_in_4bit` directly)
- merging: restore missing/unexpected unpack from load_state_dict and log
  both directions of mismatch — useful when diagnosing a bad merge
- test_benchmark: replace `abs(... - 0.60) < 0.01` with
  `pytest.approx(0.60, abs=0.01)` for consistency
- test_cli_phase10: estimated_gb assertion uses pytest.approx instead of
  exact float equality

Skipped:
- test_safety_advanced patch target — reviewer suggestion is incorrect.
  `safety.py` does NOT have a top-level `from transformers import
  pipeline`; the import lives inside `run_safety_evaluation`'s body, so
  `forgelm.safety.pipeline` isn't a valid attribute. The current
  `patch("transformers.pipeline", ...)` is the right target.

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three actionable findings + one config-level suppression for the bulk
B101 noise:

- pyproject.toml: add a [tool.bandit] section with exclude_dirs=["tests",
  ...] and skips=["B101"]. Codacy's Bandit pass was reporting "use of
  assert" on every test assertion (~40 hits); pytest *requires* asserts,
  so the right fix is config, not per-call suppression.

- chat.py: narrow the bare `except Exception` around `_console.file.flush()`
  to `(OSError, AttributeError)` — the actual failure modes (closed
  stdout, console without `.file`). Closes Bandit B110.

- export.py: annotate the `subprocess.run(cmd, ...)` call with
  `# noqa: S603  # nosec B603` plus a comment explaining why it's safe
  (list-form, no shell=True, paths under user control). Closes B603.

- test_deploy.py: replace the hardcoded "/tmp/Modelfile" string in
  test_deploy_result_dataclass with `tmp_path / "Modelfile"`. Closes B108
  on that one flagged line; other /tmp references in tests/ are now
  covered by the Bandit exclude_dirs.

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI failed on tests that don't run in the local sandbox (require torch +
transformers). Two regressions slipped through Wave 1.2:

- tests/test_moe_functions.py: drop the `num_experts` argument from the
  two `_apply_moe_expert_quantization(model, 4)` calls; the function
  signature is now single-arg.
- tests/test_oom_recovery.py: drop the `final_path` positional from the
  two `trainer._export_compliance_if_needed(...)` calls; the method
  signature is now `(metrics, result)`.

No production-code change. ruff clean; the local tests that *do* run
still pass (430), and the affected suites collect cleanly under skip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, gating)

Real-bug fixes:
- judge: replace the score=0 failure sentinel with score=None across
  _parse_judge_json / _call_api_judge / _call_local_judge. The old value
  was clipped up to 1.0 by _clip_judge_score and silently lowered the
  average. _clip_judge_score now passes None through; run_judge_evaluation
  averages only valid scores and reports the failure count separately.
  judge_results.json gains a per-row "judge_failed" boolean.
- safety: mark classifier-crash rows as low_confidence=True (with a
  classifier_error message) so they show up in the manual-review surface
  instead of disappearing into the unsafe bucket.
- safety: drop the misleading `del model` from _release_model_from_gpu
  (it only dropped the local arg binding, not the caller's reference).
  Document the contract in the docstring; the caller now sets its own
  `model = None` after calling the helper so VRAM can actually be reclaimed.
- trainer: fuzzy GPU pricing match now sorts by descending name length so
  more-specific entries beat shorter substrings (e.g. "NVIDIA H100" wins
  over "NVIDIA H1").
- trainer: _apply_benchmark_result now respects evaluation.auto_revert
  (mirroring the safety/judge gates). Benchmark failure with auto_revert=
  False is recorded on TrainResult but no longer destroys saved artifacts.
- webhook: _mask now redacts userinfo and query strings via urlparse,
  returning scheme://host plus a single safe path segment. Removed
  resp.text from the HTTP-error log so receiver-echoed payloads can no
  longer leak secrets — only status code is surfaced.

Cleanups / nits also applied:
- config: drop the redundant `self.lora and ...` truthiness check (lora
  is a required pydantic model, always truthy).
- data: _shuffle_and_passthrough and _format_sft_dataset only shuffle the
  "train" split now — validation/test stay in source order so eval
  metrics line up sample-by-sample across runs.
- data: _process_user_assistant_format distinguishes "wrong schema"
  (raise) from "empty batch" (return empty list) by checking key presence
  instead of column truthiness.
- model_card: _build_metrics_table filters any namespaced key (anything
  with `/`) so safety/, resource/, judge/, benchmark/ each stay in their
  dedicated sections instead of cluttering the metrics table.
- synthetic: log a warning on empty teacher response; skip the trailing
  api_delay after the last prompt; _load_file_responses now warns when
  seed_file is unset/missing and debug-logs malformed lines with line
  numbers so users have something to grep for.
- wizard: _save_config_to_file no longer retries the same path on OSError
  (which would re-raise the same error when the user typed the literal
  fallback name). Falls back to a timestamped file in the user's home
  directory instead.
- cli: extract _report_training_error to centralize the
  json-vs-human-log split for both ImportError and Exception paths.
  Extract _add_common_subparser_flags to deduplicate the --quiet /
  --log-level / --output-format triples across chat/export/deploy.

Tests:
- test_judge_functions: update three assertions for the new None sentinel
  (test_invalid_json_returns_none_sentinel, test_empty_string,
  test_api_timeout). Behavior change is intentional — score=0 was hiding
  failures in the average.

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral regression)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…izard CC)

Workflow security:
- nightly.yml / publish.yml: drop the workflow-level `contents: read`
  permission and declare it per-job. Each job now spells out exactly the
  scopes it needs (test jobs: read; notify-failure: read+issues; publish-
  testpypi/pypi: read+id-token).

export.py:
- Extract `_CONVERTER_SCRIPT` constant (was the literal "convert_hf_to_
  gguf.py" repeated 3×).
- Rename the `format=` keyword-only parameter to `format_=` so it no
  longer shadows the builtin. Updates `cli._run_export_cmd` and
  `tests/test_export.py::test_unsupported_format_returns_failure` to the
  new keyword.
- Refactor `export_model` (CC 25 → well below 15) by extracting:
  * `_validate_export_request` — early rejection for bad format/quant
  * `_resolve_kquant_path` — K-quant → .f16.gguf rerouting
  * `_build_converter_command` — argv assembly
  * `_run_converter` — subprocess invocation + error reporting
  * `_cleanup_merged_dir` — temporary adapter-merge cleanup

deploy.py:
- `generate_deploy_config` had 18 parameters (limit 13). Group the
  HF-Endpoints knobs (task/instance_size/instance_type/region/framework/
  vendor) into a new `HFEndpointsOptions` dataclass and the sampling
  defaults (temperature/top_k/top_p) into `SamplingOptions`. Net 18→11.
- Update `cli._run_deploy_cmd` to pass `hf_endpoints=HFEndpointsOptions
  (vendor=...)` and the test `test_json_roundtrip_for_hf_endpoints`
  similarly. CHANGELOG gains a Breaking entry for the new signature.

wizard.py:
- `run_wizard` was still over the CC threshold after Wave 4. Extract:
  * `_select_model` — model picker (POPULAR_MODELS + custom)
  * `_derive_strategy_flags` — decode the 4-way strategy menu
  * `_parse_trainer_type` — return (trainer_type, dataset_format_hint)
  * `_collect_galore_config` — the GaLore-specific interactive branch

tests/test_inference.py:
- Hoist the inline `_Tensor`/`_Scalar` stub classes (and their helper
  ops `softmax`/`log`/`sum`) to module scope. `_make_torch_stub` now
  just builds the torch module (CC 36 → well under threshold).
- Five test methods that did `torch_stub, _Tensor, _ = _make_torch_stub()`
  now do `torch_stub = _make_torch_stub()` — the local-variable
  PascalCase warnings disappear because `_Tensor` is no longer rebound
  in test scope.
- Replace the unused `tok` in `test_adapter_is_merged` with `_`.

tests/test_export.py:
- `test_no_error_when_manifest_missing` no longer hardcodes
  `/tmp/model.gguf`; uses `tmp_path / "model.gguf"`.

CHANGELOG: document the two breaking-API changes (export_model `format=`
→ `format_=`; generate_deploy_config grouped dataclasses).

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped (no behavioral regression)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… judge edges)

Real bugs:
- fit_check: when galore_enabled=True, the previous code added
  adapter_gb on top of galore_gb and sized optimizer_state_gb against
  the LoRA adapter — double-counting and wrong baseline. The two
  strategies are mutually exclusive: under GaLore we now derive
  projected_params = num_params * (galore_rank / hidden_size) and use
  THAT as trainable_params for _optimizer_state_gb; adapter_gb stays 0.
  Under LoRA we keep the original adapter sizing and skip galore_gb.
- fit_check: quant detection used to read bnb_4bit_compute_dtype even
  when load_in_4bit was False, mis-tagging plain bf16 runs. Now picks
  storage dtype: load_in_8bit → "8bit"; otherwise checks
  m.torch_dtype / m.config.torch_dtype with a small fp16/bf16/fp32 map;
  bnb_4bit_compute_dtype is consulted only when load_in_4bit=True.
- judge: an entire run with all-failures used to fall through to
  "Average judge score (0.00) below minimum (X.XX)". Now we detect
  ``not valid_scores`` first and emit a distinct failure_reason
  ("No valid judge scores (all N/M parses/requests failed)").

Type / cleanup:
- judge: JudgeResult.scores typed as List[Optional[float]] with a
  comment documenting the None sentinel — it was already storing None
  but the annotation said List[float].
- merging: drop the redundant dim=None on torch.quantile; flat is
  already 1-D after .flatten() so the default behavior is correct.
- export: rename the keyword-only parameter format_ → output_format
  (matches the convention discussed in review); update cli call site
  and test.

Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three security notices flagged by Codacy after the round-7 refactor
shifted line numbers:

- export.py:23 (B404, "Consider possible security implications
  associated with the subprocess module"): annotate the import line
  with `# noqa: S404  # nosec B404` and a short comment pointing
  reviewers to the safe-usage rationale on _run_converter.

- export.py:271-272 (B603/S603, S607): the multi-line subprocess.run
  call was already noqa'd on its opening line, but Codacy was
  re-flagging the `cmd,` continuation line on its own. Add the
  suppression on both lines (S603/B603 and S607/B607 covered) and
  expand the inline comment to spell out the no-shell, absolute-path
  invariant.

Skipped (Codacy "Possibly found usage of AI tooling: PyTorch" notices
on export.py:131, fit_check.py:240, inference.py 10x, merging.py:175):
this is Codacy's AI/ML library-usage *policy* signal, not a security
or quality issue. ForgeLM is an LLM fine-tuning toolkit; using PyTorch
is unavoidable. No code change.

Verification:
- ruff format && ruff check: clean
- pytest tests/test_export.py: 18 passed (no behavioral change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notebooks:
- New: notebooks/post_training_workflow.ipynb. End-to-end walkthrough of
  the four Phase 10 CLI commands (`forgelm --fit-check` / `chat` /
  `export` / `deploy`) plus the programmatic `forgelm.inference` API.
  Existing training notebooks deliberately left alone — their inline
  `model.generate(...)` is a fast in-memory sanity check; the new
  toolchain reloads from disk and is post-training scope.
- quickstart_sft.ipynb: append a "Next Steps — Phase 10 Post-Training
  Toolchain" section pointing into the new notebook.

Roadmap close-out:
- roadmap.md: split "Latest release" (v0.4.0, shipped 2026-04-26) from
  "Current milestone" (v0.4.5 / Phase 10.5 Quickstart). Previously both
  pointed at v0.4.0, which was misleading once the release went live.
- releases.md: correct release date 2026-04-25 → 2026-04-26 (matches
  the actual PyPI publish + GitHub Release timestamps); link to the
  GitHub release page; refresh the chat feature row to reflect the
  round-2 removal of the unimplemented `--safety` flag and the round-6
  rich-markup escape; refresh export to call out the K-quant honest-
  intermediate flow; refresh fit-check to mention LoRA⊕GaLore
  mutual exclusion and word-bounded size-hint matching; refresh
  deploy to mention HF-Hub-ID rejection for tgi/ollama.
- completed-phases.md: already accurate (Phase 10 row shows ✅ + v0.4.0).

Tooling:
- .gitignore: drop .claude/scheduled_tasks.lock and .claude/state/ —
  Claude Code runtime artifacts that shouldn't be tracked.
One-command bundled templates with opinionated defaults — primary
community-growth driver. `forgelm quickstart customer-support` renders
a config the existing trainer accepts unchanged, copies a small seed
dataset, optionally runs training, and drops into chat with the result.

Module: forgelm/quickstart.py — Template registry (frozen dataclass),
auto_select_model() GPU-aware downsizing (≥10 GB → primary, otherwise
fallback ≤2B; explicit no-gpu-detected reason without CUDA), and
run_quickstart() orchestrator that substitutes model + dataset paths
into the bundled YAML.

CLI: forgelm quickstart <template> with --list (text + JSON), --model
/ --dataset overrides, --dry-run, --no-chat. Wizard now opens with
"Start from a template?" — Yes routes to quickstart, No falls through
to the existing 8-step flow. Same code paths, same YAML schema.

5 templates under forgelm/templates/ (all QLoRA 4-bit, rank=8, batch=1):
- customer-support: Qwen2.5-7B ↔ SmolLM2-1.7B SFT, 60 examples
- code-assistant:   Qwen2.5-Coder-7B ↔ SmolLM2-1.7B SFT, 60 examples
- domain-expert:    BYOD (empty data + walkthrough README)
- medical-qa-tr:    Qwen2.5-7B Turkish medical Q&A with disclaimers
- grpo-math:        Qwen2.5-Math-7B ↔ -1.5B GRPO, 40 prompts

LICENSES.md catalogs all bundled seed datasets (CC-BY-SA 4.0,
author-original) and documents how to contribute new templates.
pyproject.toml [tool.setuptools.package-data] bundles *.yaml /
*.jsonl / *.md under forgelm.templates into the wheel.

Tests: tests/test_quickstart.py — 40 tests, GPU-independent via
_detect_available_vram_gb mock. Includes a regression test that
loads every generated YAML through load_config() — strongest guard
against template drift the moment a config schema changes. Nightly
CI runs `quickstart --dry-run` + `--config <out> --dry-run` for
every bundled template.

Docs: roadmap.md/-tr.md + phase-12-quickstart.md + releases.md mark
Phase 10.5 as Done; CHANGELOG and README updated; docs/guides/
quickstart.md gets an "Option 0: One-Command Quickstart Template"
section. Version bumped to 0.4.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cemililik has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 24 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 24 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5dac36ba-8638-439f-bb3b-cf429cdf1267

📥 Commits

Reviewing files that changed from the base of the PR and between a933021 and 1319d57.

📒 Files selected for processing (4)
  • forgelm/cli.py
  • forgelm/grpo_rewards.py
  • forgelm/quickstart.py
  • tests/test_quickstart_hardening.py
📝 Walkthrough

Walkthrough

Adds a Quickstart Layer (v0.4.5): bundled one-command templates shipped in the wheel, forgelm quickstart CLI and wizard integration, VRAM-aware model selection and config materialization, GRPO reward fallbacks and trainer wiring, many templates/tests, and nightly/wheel-install CI smoke jobs.

Changes

Cohort / File(s) Summary
Version & Packaging
forgelm/__init__.py, pyproject.toml
Bump version 0.4.0→0.4.5 and include forgelm.templates package-data so template assets ship in built wheels.
Quickstart Core
forgelm/quickstart.py
New immutable Template model and TEMPLATES registry; discovery APIs, auto_select_model (VRAM-aware), run_quickstart that materializes configs/datasets, writes provenance/audit JSONL, and helpers to format/summarize results.
CLI & Wizard
forgelm/cli.py, forgelm/wizard.py
Add forgelm quickstart subcommand (--list/JSON, --model/--dataset/--output, --dry-run, --no-chat) with subprocess wiring and flag forwarding; wizard first attempts quickstart flow with BYOD validation and falls back to full wizard.
Templates & Assets
forgelm/templates/..., forgelm/templates/LICENSES.md, forgelm/templates/__init__.py
Add five bundled templates (code-assistant, customer-support, domain-expert, grpo-math, medical-qa-tr) with configs/data/README and a licenses table; package docstring for reliable resource resolution.
Trainer / GRPO Rewards
forgelm/grpo_rewards.py, forgelm/trainer.py
New GRPO reward fallbacks (format_match_reward, length_shaping_reward, combined_format_length_reward); trainer wiring always includes shaping reward and conditionally appends correctness reward when gold_answer exists; answer-normalization helpers and GRPO length field rename.
Config & Data
forgelm/config.py, forgelm/data.py, config_template.yaml, docs/reference/configuration.md
Canonical grpo_max_completion_length with alias grpo_max_new_tokens via Field alias; tighten dataset pairing to zip(..., strict=True); docs/comments updated.
Inference / Chat
forgelm/inference.py, forgelm/chat.py
Lazy-import torch, run model.generate inside torch.inference_mode() in background worker; ensure chat /save creates parent directory before writing.
Fit check / Utilities
forgelm/fit_check.py
Refactor VRAM estimation into helpers: quant-scheme selection, component breakdown, CUDA probing, and verdict mapping; preserves public APIs.
Merging / Judge
forgelm/merging.py, forgelm/judge.py
Minor merge tensor dim fix; refactor judge scoring into prompt-scoring and summarization helpers preserving result semantics.
Tests & Packaging QA
tests/... (many new/updated tests)
Extensive test additions: quickstart registry/rendering, VRAM/no-GPU behavior, packaging/wheel asset presence, CLI subprocess forwarding, GRPO reward unit/integ tests, quickstart hardening/regression, wizard BYOD, and packaging smoke tests.
Docs & Roadmap
README.md, CHANGELOG.md, docs/guides/quickstart.md, docs/roadmap*.md
Document quickstart usage and v0.4.5 release; update guides/roadmap to mark Phase 10.5 complete and describe templates, licensing, and CI.
CI
.github/workflows/nightly.yml
Add template-level dry-run smoke in nightly job and a wheel-install-smoke job that builds/installs the wheel in an isolated venv and validates packaged templates; include it in failure notifications.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / CLI
    participant CLI as forgelm.cli
    participant QS as forgelm.quickstart
    participant Train as Trainer Process
    participant Config as Config Loader

    User->>CLI: forgelm quickstart <template> [--dry-run/overrides]
    CLI->>QS: run_quickstart(template, overrides)
    QS->>QS: detect GPU VRAM / auto_select_model
    QS->>QS: load template config.yaml & patch model/data
    QS->>QS: copy bundled data.jsonl (if present)
    QS->>QS: write timestamped YAML + audit JSONL -> return QuickstartResult
    alt dry-run
        CLI-->>User: print summary / JSON
    else normal
        CLI->>Train: spawn subprocess: forgelm --config <generated.yaml>
        Train->>Config: load generated YAML
        Train-->>CLI: exit status
        CLI->>Train: optionally spawn: forgelm chat <model_dir>
        Train-->>User: interactive chat
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 A hop, a template, a tiny plan,

Seeds and YAML sown by rabbit's hand.
VRAM-wise I pick the best, then run—
One command, one config, training begun.
Quickstarts bloom beneath the moonlit land.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: Phase 10.5 Quickstart Layer (v0.4.5)' clearly and concisely summarizes the main change: introducing a quickstart layer feature for Phase 10.5 with version 0.4.5.
Description check ✅ Passed The pull request description comprehensively covers Summary, Changes, Type, Testing, and Checklist sections. All required sections are present and filled out with appropriate detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented Apr 26, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 critical · 1 minor

Alerts:
⚠ 3 issues (≤ 0 issues of at least minor severity)

Results:
3 new issues

Category Results
BestPractice 1 minor
Security 2 critical

View in Codacy

🟢 Metrics 361 complexity · 20 duplication

Metric Results
Complexity 361
Duplication 20

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Quickstart Layer and Post-Training toolchain, adding bundled templates, an interactive chat REPL, GGUF export, VRAM estimation, and deployment config generation. It also includes security hardening for webhooks and Markdown templates, along with refactoring for better modularity and OOM recovery. Feedback focuses on enhancing robustness, specifically by ensuring directory creation for saved chat history, handling interrupts in the quickstart CLI, guarding against missing configuration sections, enforcing strictness in dataset zipping, and ensuring inference mode is used in background generation threads.

I am having trouble creating individual review comments. Click here to see my feedback.

forgelm/chat.py (181)

medium

The /save command will fail if the provided path includes non-existent parent directories. It's safer to ensure the directory structure exists before attempting to write the file.

            import os
            if os.path.dirname(path):
                os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
            with open(path, "w", encoding="utf-8") as f:

forgelm/cli.py (909-911)

medium

The quickstart subcommand should handle KeyboardInterrupt gracefully, similar to the chat subcommand, to avoid showing a traceback when the user cancels the operation (especially during the long-running training phase).

    elif command == "quickstart":
        try:
            _run_quickstart_cmd(args, getattr(args, "output_format", "text"))
        except KeyboardInterrupt:
            pass
        sys.exit(EXIT_SUCCESS)

forgelm/config.py (472)

medium

Accessing self.lora.r without checking if self.lora is None could lead to an AttributeError if the LoRA configuration section is omitted from the YAML. While other parts of the codebase might assume its presence, it's safer to guard this check.

        if self.lora and self.lora.r > 0:

forgelm/data.py (121)

medium

Using zip without strict=True can lead to silent data truncation if the System, User, and Assistant columns have mismatched lengths. Enabling strict mode ensures that such inconsistencies are caught early.

        for s, u, a in zip(sys_texts, user_texts, asst_texts, strict=True)

forgelm/inference.py (288-290)

medium

The background generation thread should run within torch.inference_mode() to disable gradient tracking and reduce memory overhead, consistent with the non-streaming generate implementation.

    def _gen_thread() -> None:
        import torch
        try:
            with torch.inference_mode():
                model.generate(**gen_kwargs)

- chat.py /save: create parent directories before writing transcript so
  user-supplied paths with non-existent parents don't error mid-write.
- cli.py quickstart subcommand: catch KeyboardInterrupt so Ctrl-C
  during the (long) training subprocess exits cleanly without a
  traceback, matching the chat subcommand.
- config.py GaLore-with-LoRA log: guard `self.lora.r` access behind a
  None check so a YAML omitting the lora section doesn't AttributeError.
- data.py User/Assistant zip: use strict=True so mismatched column
  lengths fail loudly instead of silently truncating training data.
- inference.py generate_stream: wrap the background generation thread
  in torch.inference_mode() to disable gradient tracking, matching the
  non-streaming generate() implementation.
- cli.py quickstart subprocess: explicit security-rationale comment
  on subprocess import — argv-list form (not shell=True) means no
  shell is invoked, defusing recurring Codacy B404/B603 advisories.

All findings verified against current code before fixing. No behavior
changes for the happy path; failure modes get clearer or safer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The 3 tests in TestGrpoRewardCallable patch trl.GRPOTrainer, which
mock.patch resolves via getattr before swapping. Newer trl releases
lazy-load the GRPO module, and that lazy import fails on torch
versions that don't expose torch.distributed.fsdp.FSDPModule (a moving
target in the torch ↔ trl pairing). The patch itself raises
AttributeError before our test code runs, surfacing as a false fail
in nightly compatibility CI.

Probe trl.GRPOTrainer at module load and skip the whole class when
it can't be reached. Tests still run on environments with a
compatible torch+trl pairing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/roadmap-tr.md (1)

22-38: ⚠️ Potential issue | 🟡 Minor

Mermaid diagram is stale relative to the updated status table.

The table now references "Faz 10.5" (with target v0.4.5) and adds a new "Faz 14" (target v0.5.1), but the diagram still labels the quickstart node as P12 Quickstart Katmanı and omits Faz 14 entirely. Also, the AI summary mentions a styling change for P105 that is not present in the diagram code below.

Consider relabeling P12 → P105 (Faz 10.5) and adding a Faz 14 node so the visual matches the new "Bir bakışta durum" table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/roadmap-tr.md` around lines 22 - 38, Update the Mermaid diagram so it
matches the status table: rename node P12 (label "Quickstart Katmanı") to P105
(Faz 10.5) and change its target tag to v0.4.5, add a new node for Faz 14 (e.g.,
P14 with label "Faz 14" and target v0.5.1) and wire the edges the same way P12
was connected (P10 → P105, P11 → P105, P105 → P13), and apply the intended style
change mentioned in the summary for P105 (the same fill/stroke scheme used for
other Faz 10.x nodes).
🧹 Nitpick comments (6)
forgelm/chat.py (1)

182-184: Directory pre-creation is fine; the if parent: guard is effectively dead.

Because os.path.abspath(path) always resolves to an absolute path, os.path.dirname(...) is never an empty string, so the guard never short-circuits. Not a bug — keep it for clarity if you prefer, or drop it. The surrounding OSError handler already covers makedirs failures, so error paths are correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/chat.py` around lines 182 - 184, The guard "if parent:" before
creating directories is dead because os.path.dirname(os.path.abspath(path))
never yields empty string; remove the conditional and call os.makedirs(parent,
exist_ok=True) unconditionally (or if you prefer keep it, replace it with an
explanatory comment) — update the block around where parent is computed (using
os.path.dirname and os.path.abspath) in forgelm/chat.py so only the necessary
os.makedirs(parent, exist_ok=True) remains and rely on the existing OSError
handling for failures.
forgelm/config.py (1)

472-479: The self.lora and guard is unreachable: lora is a required field on ForgeConfig.

ForgeConfig.lora: LoraConfigModel (line 373) is required, so self.lora is always truthy after validation — the new existence check never short-circuits. This is harmless, but if the goal was to handle a missing lora section then lora should be made Optional[LoraConfigModel] = None on ForgeConfig for the guard to actually do anything. Otherwise drop the redundant check.

♻️ Option A — drop the dead guard
-        if self.lora and self.lora.r > 0:
+        if self.lora.r > 0:
             logger.info(
                 "GaLore (gradient rank=%d) enabled alongside LoRA (adapter rank=%d). "
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/config.py` around lines 472 - 479, The conditional "if self.lora and
self.lora.r > 0:" contains a redundant existence check because ForgeConfig.lora
is required (type LoraConfigModel), so remove the dead "self.lora and" guard and
change the condition to "if self.lora.r > 0:" (keeping the logger message that
references self.training.galore_rank and self.lora.r); alternatively, if you
intentionally want lora to be optional, change the ForgeConfig.lora type to
Optional[LoraConfigModel] = None and keep the original guard—pick one approach
and apply it consistently.
forgelm/wizard.py (1)

318-378: Consider extracting the template-pick step.

_maybe_run_quickstart_template() interleaves the prompt, the digit/name parsing, the BYOD handling, and the orchestrator call in one function — and combined with the new branching in run_wizard() it pushes cognitive complexity over the SonarCloud threshold (flagged on line 381). Extracting the template-resolution block (lines 337‑354) into a small helper such as _pick_template_name(names) -> Optional[str] keeps each piece focused and quiets the static analysis warning without changing behavior.

Also, a small UX nit (no fix required): when the user types a digit that is out of range (e.g., "99"), the function silently falls through to the full 8‑step wizard rather than re-prompting. If that wasn't intentional, a single re-prompt would be friendlier than dumping into the long flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/wizard.py` around lines 318 - 378, Extract the template
selection/parsing logic inside _maybe_run_quickstart_template into a small
helper function named _pick_template_name(names) -> Optional[str] that accepts
the list of template names and returns the chosen name or None; move the block
that reads raw = _prompt(...), parses digits/indices and checks membership in
TEMPLATES into _pick_template_name and call it from
_maybe_run_quickstart_template (keep the same return semantics so behavior is
unchanged). Ensure the helper uses the same prompt text and default (names[0])
and preserves the current checks (raw.isdigit() -> index bounds -> names[idx-1],
elif raw in TEMPLATES -> raw, else None) so SonarCloud cognitive complexity is
reduced without changing UX; do not change BYOD handling or exception behavior.
forgelm/quickstart.py (3)

199-205: Address Sonar nits in _detect_available_vram_gb.

free is unused (Sonar python:S1481). Rename to _ to silence the warning and signal intent.

♻️ Proposed fix
-        free, total = torch.cuda.mem_get_info()
+        _, total = torch.cuda.mem_get_info()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/quickstart.py` around lines 199 - 205, In _detect_available_vram_gb,
rename the unused variable free returned by torch.cuda.mem_get_info() to _, so
the tuple unpacking becomes "_, total = torch.cuda.mem_get_info()" to silence
the Sonar python:S1481 unused-variable warning and signal intent while
preserving logic that uses total for the VRAM capacity calculation; keep the
existing exception handling and logger.debug intact.

318-330: Unused color parameter on format_template_list.

The color keyword-only argument is accepted but never read — Sonar python:S1172. Either drop it (no caller uses it today) or wire it up to ANSI rendering. Dropping is the lower-risk option since the public callers (forgelm/cli.py::_run_quickstart_cmd) invoke it without arguments.

♻️ Proposed fix
-def format_template_list(*, color: bool = False) -> str:
+def format_template_list() -> str:
     """Render the template registry as a human-readable list."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/quickstart.py` around lines 318 - 330, Remove the unused keyword-only
parameter `color` from the function signature of format_template_list and update
any internal references if present; ensure callers like _run_quickstart_cmd in
forgelm/cli.py continue to call format_template_list() without arguments (no
changes needed there because they don't pass color), and run tests/lint to
confirm the Sonar S1172 warning is resolved.

282-285: Defer config_target.parent.mkdir until after dataset resolution succeeds.

Today, calling run_quickstart("domain-expert") without dataset_override creates ./configs/ (or the user's --output parent directory) and only then raises ValueError("does not bundle a dataset"). The user is left with an empty directory they didn't ask for. Move the mkdir to just before the YAML write, or perform dataset resolution first.

♻️ Proposed reorder
     config_target = Path(output_path) if output_path else _default_output_path(template_name)
-    config_target.parent.mkdir(parents=True, exist_ok=True)
-
     dataset_path, dataset_notes = _resolve_dataset(template, dataset_override, config_target.parent)
 
     cfg = _materialize_config(template, chosen_model, dataset_path)
 
+    config_target.parent.mkdir(parents=True, exist_ok=True)
     with open(config_target, "w", encoding="utf-8") as f:
         yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False)

Note: _resolve_dataset already calls scratch_dir.mkdir(...) itself before copying the bundled JSONL, so the bundled-data path keeps working.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/quickstart.py` around lines 282 - 285, The current code creates the
output parent directory via config_target.parent.mkdir(...) before calling
_resolve_dataset(...), which leaves an empty directory on failure; move the call
to config_target.parent.mkdir(parents=True, exist_ok=True) so it runs only after
dataset resolution succeeds (i.e., after dataset_path, dataset_notes =
_resolve_dataset(template, dataset_override, config_target.parent)) and
immediately before the YAML/config write step, or remove the early mkdir
entirely since _resolve_dataset() creates its own scratch dir when using bundled
data; update references to config_target, output_path, _default_output_path, and
dataset_override accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/nightly.yml:
- Around line 63-67: The loop in the nightly workflow intentionally omits the
BYOD template "domain-expert" because template_assets() (in
forgelm/quickstart.py) raises a ValueError for templates without bundled
datasets; add an inline comment inside the for tpl loop explaining that
domain-expert is BYOD and therefore excluded to prevent confusion, and also
update the PR summary text to accurately say "smoke tests 4 of 5 templates"
instead of "all 5 templates".

In `@CHANGELOG.md`:
- Line 7: The changelog has duplicate dates for release headings "## [0.4.5] —
2026-04-25" and "## [0.4.0] — 2026-04-25"; verify the correct release date for
the 0.4.5 entry and update the "## [0.4.5] — 2026-04-25" header to the correct
date (likely "2026-04-26") so it matches the PR/tag date, or correct the 0.4.0
date if that one is wrong, ensuring the header text for the affected version(s)
exactly matches the intended release date.

In `@docs/roadmap.md`:
- Around line 16-18: The roadmap has inconsistent release dates between the two
entries: the "v0.4.5" line shows 2026-04-25 while the "v0.4.0" line shows
2026-04-26, which is impossible; update the date for the "v0.4.5" entry (the
`v0.4.5` string in docs/roadmap.md) to the correct shipping date (likely
2026-04-26) so the timeline reads chronologically, or if  v0.4.0 is wrong update
the `v0.4.0` date instead—edit the text around the `v0.4.5` / `v0.4.0` entries
to make the dates consistent.

In `@docs/roadmap/releases.md`:
- Line 61: The release entry header "v0.4.5 — \"Quickstart Layer\" (2026-04-25)"
has a date earlier than the v0.4.0 entry and appears to be a typo; update the
header in releases.md so the date is consistent (e.g., change "(2026-04-25)" to
"(2026-04-26)" or set the correct published date), or if the version is wrong
adjust the version string to the intended one, ensuring the header "v0.4.5 —
\"Quickstart Layer\"" and its date match the PR/release timeline.

In `@forgelm/cli.py`:
- Around line 854-878: The spawned subprocesses (train_cmd and chat_cmd) don’t
inherit top-level CLI flags from args, so propagate relevant flags (e.g.
--output-format, --quiet, --log-level, --offline) into both train_cmd and
chat_cmd before calling subprocess.run; for each flag on args, append the
appropriate argv form (use flag-only for booleans like --quiet/--offline when
True, and flag + value for options like --output-format and --log-level) to the
lists so the child processes run with the same settings as the parent (locate
where train_cmd and chat_cmd are built near the call to
_load_config_for_quickstart_chat and update them accordingly).
- Around line 866-874: The code currently hardcodes "final_model" when building
final_model_dir; instead load the generated YAML via
_load_config_for_quickstart_chat(result.config_path) into a config dict and
derive the path using the config's output_dir and training.final_model_dir
(falling back to "final_model" if the field is missing). Replace the current
final_model_dir = Path(_load_config_for_quickstart_chat(result.config_path)) /
"final_model" with logic that reads config["output_dir"] and
config.get("training", {}).get("final_model_dir", "final_model") (mirroring how
_build_dry_run_result constructs the trainer output path) so auto-chat will find
the model when templates override final_model_dir.

In `@forgelm/templates/grpo-math/data.jsonl`:
- Line 26: Q26's prompt "Three numbers add to 60. The first is twice the second,
and the third is 5 more than the second..." yields a fractional second number
(13.75); change the constants so the answer is an integer — for example replace
"60" with "65" in the prompt so the equation 4x+5=65 gives x=15 and the final
line remains "Answer: <number>" for exact-match extraction.

In `@forgelm/templates/LICENSES.md`:
- Around line 30-31: The contributing checklist requires bundled template
configs to set gradient_checkpointing: true, but the added templates
(medical-qa-tr/config.yaml, code-assistant/config.yaml,
domain-expert/config.yaml, etc.) do not set training.gradient_checkpointing;
either add gradient_checkpointing: true under the training: section of each
bundled template config mentioned so the 12 GB-fit claim is accurate, or update
the checklist wording in LICENSES.md to remove the strict requirement (e.g., say
"rely on trainer default unless you need to override"); pick one approach and
apply it consistently across the listed templates and the checklist.

In `@forgelm/templates/medical-qa-tr/data.jsonl`:
- Line 12: Update the assistant message string in the JSON object that answers
"Yüksek ateş nasıl düşürülür?" to correct the typo "sertboyun" to two words
"sert boyun" so the model trains on the proper Turkish phrase; locate the
assistant content field inside the JSON entry (the object where
"role":"assistant" contains the response text) and replace the single-word
"sertboyun" with "sert boyun" preserving the rest of the sentence and
punctuation.
- Line 17: The assistant's phrase "üzerini temiz bir bez veya ileri tabakanın
yapışmadığı pansumanla örtün" is awkward and should be replaced with a concise
Turkish phrasing meaning "non‑stick dressing"; update the assistant message
content (the string starting with "Hafif (1. derece) yanıkta:") to read
something like "... üzerini temiz bir bez veya yara yüzeyine yapışmayan
pansumanla örtün." ensuring the rest of the sentence and the final safety
reminder ("Tıbbi acil durumlarda 112'yi arayın veya bir sağlık profesyoneline
danışın.") remain unchanged.

In `@forgelm/wizard.py`:
- Around line 357-361: The BYOD branch in _maybe_run_quickstart_template
currently treats an empty dataset_path as a silent fallback which causes
run_wizard to start the generic 8-step flow; change this to re-prompt the user
(or abort with a clear message) when template.bundled_dataset is false and the
user submits an empty path: replace the single _prompt call for dataset_path
with a loop that calls _prompt repeatedly (showing the same prompt and an
explicit "cancel" or empty-to-abort option) until a non-empty path is provided
or the user explicitly cancels, and if cancelled raise/return a clear sentinel
(or error) rather than returning None so run_wizard can handle it appropriately;
reference: template.bundled_dataset, chosen, _prompt,
_maybe_run_quickstart_template, run_wizard, and the BYOD template like
"domain-expert".

---

Outside diff comments:
In `@docs/roadmap-tr.md`:
- Around line 22-38: Update the Mermaid diagram so it matches the status table:
rename node P12 (label "Quickstart Katmanı") to P105 (Faz 10.5) and change its
target tag to v0.4.5, add a new node for Faz 14 (e.g., P14 with label "Faz 14"
and target v0.5.1) and wire the edges the same way P12 was connected (P10 →
P105, P11 → P105, P105 → P13), and apply the intended style change mentioned in
the summary for P105 (the same fill/stroke scheme used for other Faz 10.x
nodes).

---

Nitpick comments:
In `@forgelm/chat.py`:
- Around line 182-184: The guard "if parent:" before creating directories is
dead because os.path.dirname(os.path.abspath(path)) never yields empty string;
remove the conditional and call os.makedirs(parent, exist_ok=True)
unconditionally (or if you prefer keep it, replace it with an explanatory
comment) — update the block around where parent is computed (using
os.path.dirname and os.path.abspath) in forgelm/chat.py so only the necessary
os.makedirs(parent, exist_ok=True) remains and rely on the existing OSError
handling for failures.

In `@forgelm/config.py`:
- Around line 472-479: The conditional "if self.lora and self.lora.r > 0:"
contains a redundant existence check because ForgeConfig.lora is required (type
LoraConfigModel), so remove the dead "self.lora and" guard and change the
condition to "if self.lora.r > 0:" (keeping the logger message that references
self.training.galore_rank and self.lora.r); alternatively, if you intentionally
want lora to be optional, change the ForgeConfig.lora type to
Optional[LoraConfigModel] = None and keep the original guard—pick one approach
and apply it consistently.

In `@forgelm/quickstart.py`:
- Around line 199-205: In _detect_available_vram_gb, rename the unused variable
free returned by torch.cuda.mem_get_info() to _, so the tuple unpacking becomes
"_, total = torch.cuda.mem_get_info()" to silence the Sonar python:S1481
unused-variable warning and signal intent while preserving logic that uses total
for the VRAM capacity calculation; keep the existing exception handling and
logger.debug intact.
- Around line 318-330: Remove the unused keyword-only parameter `color` from the
function signature of format_template_list and update any internal references if
present; ensure callers like _run_quickstart_cmd in forgelm/cli.py continue to
call format_template_list() without arguments (no changes needed there because
they don't pass color), and run tests/lint to confirm the Sonar S1172 warning is
resolved.
- Around line 282-285: The current code creates the output parent directory via
config_target.parent.mkdir(...) before calling _resolve_dataset(...), which
leaves an empty directory on failure; move the call to
config_target.parent.mkdir(parents=True, exist_ok=True) so it runs only after
dataset resolution succeeds (i.e., after dataset_path, dataset_notes =
_resolve_dataset(template, dataset_override, config_target.parent)) and
immediately before the YAML/config write step, or remove the early mkdir
entirely since _resolve_dataset() creates its own scratch dir when using bundled
data; update references to config_target, output_path, _default_output_path, and
dataset_override accordingly.

In `@forgelm/wizard.py`:
- Around line 318-378: Extract the template selection/parsing logic inside
_maybe_run_quickstart_template into a small helper function named
_pick_template_name(names) -> Optional[str] that accepts the list of template
names and returns the chosen name or None; move the block that reads raw =
_prompt(...), parses digits/indices and checks membership in TEMPLATES into
_pick_template_name and call it from _maybe_run_quickstart_template (keep the
same return semantics so behavior is unchanged). Ensure the helper uses the same
prompt text and default (names[0]) and preserves the current checks
(raw.isdigit() -> index bounds -> names[idx-1], elif raw in TEMPLATES -> raw,
else None) so SonarCloud cognitive complexity is reduced without changing UX; do
not change BYOD handling or exception behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83988864-811c-412d-b3ec-f7392d3a225d

📥 Commits

Reviewing files that changed from the base of the PR and between fd0838c and 993d840.

📒 Files selected for processing (31)
  • .github/workflows/nightly.yml
  • CHANGELOG.md
  • README.md
  • docs/guides/quickstart.md
  • docs/roadmap-tr.md
  • docs/roadmap.md
  • docs/roadmap/phase-12-quickstart.md
  • docs/roadmap/releases.md
  • forgelm/__init__.py
  • forgelm/chat.py
  • forgelm/cli.py
  • forgelm/config.py
  • forgelm/data.py
  • forgelm/inference.py
  • forgelm/quickstart.py
  • forgelm/templates/LICENSES.md
  • forgelm/templates/__init__.py
  • forgelm/templates/code-assistant/config.yaml
  • forgelm/templates/code-assistant/data.jsonl
  • forgelm/templates/customer-support/config.yaml
  • forgelm/templates/customer-support/data.jsonl
  • forgelm/templates/domain-expert/README.md
  • forgelm/templates/domain-expert/config.yaml
  • forgelm/templates/grpo-math/config.yaml
  • forgelm/templates/grpo-math/data.jsonl
  • forgelm/templates/medical-qa-tr/config.yaml
  • forgelm/templates/medical-qa-tr/data.jsonl
  • forgelm/wizard.py
  • pyproject.toml
  • tests/test_grpo_reward.py
  • tests/test_quickstart.py

Comment thread .github/workflows/nightly.yml
Comment thread CHANGELOG.md Outdated
Comment thread docs/roadmap.md Outdated
Comment thread docs/roadmap/releases.md Outdated
Comment thread forgelm/cli.py Outdated
Comment thread forgelm/templates/grpo-math/data.jsonl Outdated
Comment thread forgelm/templates/LICENSES.md Outdated
Comment thread forgelm/templates/medical-qa-tr/data.jsonl Outdated
Comment thread forgelm/templates/medical-qa-tr/data.jsonl Outdated
Comment thread forgelm/wizard.py
cemililik and others added 2 commits April 26, 2026 11:10
…nitpicks)

Verified each finding against current code; applied real fixes only.

Dates / consistency
- CHANGELOG.md, docs/roadmap.md, roadmap-tr.md, roadmap/releases.md,
  roadmap/phase-12-quickstart.md: v0.4.5 date corrected from
  2026-04-25 to 2026-04-26 (chronologically after v0.4.0 PyPI publish).
- docs/roadmap-tr.md: Mermaid diagram updated to match the EN version
  — adds Faz 10.5 (P105) + Faz 14 (P14) nodes with the same fill/
  stroke scheme used for completed phases.

CI / wiring
- .github/workflows/nightly.yml: inline comment explaining that the
  smoke loop covers 4 of 5 templates (domain-expert is BYOD and is
  covered separately by the pytest unit test).
- forgelm/cli.py: quickstart subcommand now propagates top-level
  flags (--output-format, --quiet, --log-level) into both the train
  and chat subprocesses so the child inherits the operator's logging
  preferences.
- forgelm/cli.py: the auto-launched chat REPL now reads
  training.final_model_dir from the generated YAML instead of
  hardcoding "final_model" — so templates that override it are still
  found. Renames _load_config_for_quickstart_chat ->
  _load_quickstart_train_paths and returns (output_dir, final_subdir).

UX
- forgelm/wizard.py: BYOD branch in _maybe_run_quickstart_template
  used to silently fall through to the full 8-step flow when the
  user hit enter at the dataset prompt. Now re-prompts in a loop
  until a real path is provided or the user types "cancel".
- forgelm/templates/LICENSES.md: contributing checklist no longer
  hard-requires `gradient_checkpointing: true` in template configs;
  trainer.py auto-enables it when CUDA is available, so explicit
  setting is only for overrides.

Data correctness
- forgelm/templates/grpo-math/data.jsonl Q26: changed "add to 60" to
  "add to 65" so the equation 4x+5=65 yields x=15 (integer) instead
  of 13.75 (fractional, breaks `Answer: <number>` exact-match).
- forgelm/templates/medical-qa-tr/data.jsonl line 12: fixed Turkish
  typo "sertboyun" -> "sert boyun" (two words).
- forgelm/templates/medical-qa-tr/data.jsonl line 17: replaced awkward
  "ileri tabakanın yapışmadığı pansumanla" with the natural Turkish
  phrasing "yara yüzeyine yapışmayan pansumanla".

Nitpicks
- forgelm/chat.py: removed dead `if parent:` guard around makedirs.
  os.path.dirname(os.path.abspath(path)) is always non-empty; the
  conditional was unreachable.
- forgelm/config.py: reverted previous defensive `self.lora and
  self.lora.r > 0` to `self.lora.r > 0`. ForgeConfig.lora is a
  required field (LoraConfigModel, no Optional, no default), so the
  None check was dead code.
- forgelm/quickstart.py: renamed `free, total =` to `_, total =` in
  _detect_available_vram_gb to silence Sonar S1481 unused-variable.
- forgelm/quickstart.py: removed unused `color` keyword-only param
  from format_template_list — no caller passes it.

Skipped (low-value)
- wizard.py helper extraction for SonarCloud cognitive-complexity
  nitpick — the function is already linear and well-commented; the
  proposed split adds an indirection without removing complexity.
- quickstart.py mkdir reordering — current placement is correct
  for the dataset_override path, where _resolve_dataset() returns
  early without creating the directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al gold-answer correctness

trl.GRPOTrainer requires reward_funcs to be set. The bundled grpo-math
quickstart used to ship without grpo_reward_model, which crashed at
GRPOTrainer init with TypeError. Two reward callables are now wired
additively (TRL sums them):

- combined_format_length_reward (forgelm/grpo_rewards.py): always-on
  baseline. 0.8 * format-match (Answer: <value> regex) + 0.2 * length
  shaping. Gradient-rich from step 0; teaches output structure even
  when no ground truth exists.
- _math_reward_fn (forgelm/trainer.py): correctness signal, appended
  only when the dataset carries a `gold_answer` field. Strips common
  units, compares with exact-string match then numeric tolerance.

The bundled grpo-math seed now has a `gold_answer` per row, so the
template trains end-to-end with both signals out of the box. When
grpo_reward_model is set, the classifier callable replaces both
built-ins (operator opted into a learned reward).

config.py docstring, docs/guides/alignment.md "Reward selection",
template config.yaml header all updated to describe the additive
behavior. CLAUDE.md registers the new grpo_rewards module.

Tests:
- test_grpo_format_reward.py — pure-Python tests for the format/length
  callables + trainer integration (skipped on torch/trl version
  mismatch via the existing grpo_patchable probe).
- test_grpo_math_reward.py — pure-Python tests for normalize/match/
  reward_fn helpers.
- test_grpo_reward.py — updated existing GRPO trainer wiring tests:
  no-reward-model + no-gold-answers now expects [combined] (was raise);
  gold_answer present now expects [combined, _math_reward_fn] (was
  [_math_reward_fn] only).
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
forgelm/quickstart.py (1)

367-447: ⚠️ Potential issue | 🟡 Minor

dry_run still produces on-disk side effects (per-run dir, audit event, dataset copy, YAML).

Lines 396, 398-413, 415, 425-427 all execute regardless of dry_run. The flag is purely informational here and only gates the trainer subprocess in forgelm.cli. Either tighten the docstring to make that explicit (e.g. "Hint forwarded to the CLI; this function always materializes config + dataset"), or short-circuit _resolve_dataset/_materialize_config/the YAML write when dry_run=True so a true preview leaves no artifacts. Same concern as the prior review on this hunk — flagging again because the behavior is unchanged in this revision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/quickstart.py` around lines 367 - 447, The quickstart function
currently performs filesystem side effects even when dry_run=True; change it to
short-circuit before any on-disk work: after deciding
chosen_model/selection_reason, if dry_run is True skip calling
_append_audit_event, skip creating config_target.parent, skip _resolve_dataset
and _materialize_config and the YAML write (and do not perform the dataset
copy), and return a QuickstartResult that reflects the
chosen_model/selection_reason with config_path and dataset_path set to None (or
a clearly non-written marker) and started_training=False; update any logfile
message to reflect dry-run state. Reference symbols: quickstart,
_append_audit_event, config_target, _resolve_dataset, _materialize_config,
QuickstartResult, and the YAML write block.
🧹 Nitpick comments (4)
forgelm/wizard.py (1)

326-378: Unused template_name parameter — drop it or surface it in prompts.

_resolve_byod_dataset_path accepts template_name but never uses it (SonarCloud confirmed). Either remove it from the signature and the caller, or thread it into the prompt/error messages so users see which template they're providing data for (e.g., "Path to your dataset JSONL for '{template_name}'…").

♻️ Suggested fix (drop the parameter)
-def _resolve_byod_dataset_path(template_name: str) -> Optional[str]:
+def _resolve_byod_dataset_path() -> Optional[str]:
     """Prompt the user for a BYOD dataset path and validate it.
-        dataset_path = _resolve_byod_dataset_path(chosen)
+        dataset_path = _resolve_byod_dataset_path()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/wizard.py` around lines 326 - 378, The function
_resolve_byod_dataset_path currently takes an unused parameter template_name;
remove this unused parameter from the function signature and its docstring and
update every caller to stop passing template_name (or, alternatively, if you
prefer to keep the context, thread template_name into the user-facing
strings—e.g., include it in the prompt and error messages inside
_resolve_byod_dataset_path—so the parameter is actually used). Ensure you update
the function definition, adjust all invocations to match the new signature (or
keep the same calls if you choose to use the parameter), and revise the
docstring to reflect the final behavior.
tests/test_cli_helpers.py (1)

35-44: Redundant filter assertions — the index check below is the real guard.

Lines 38-39 build a comprehension that filters by membership in ("--log-level", "DEBUG") and compares to the same list. This passes vacuously even if the two tokens were not adjacent (and would also pass if there were spurious copies). The contiguous-pair assertions at lines 41-44 already cover this correctly; consider dropping the comprehension lines to avoid the false sense of coverage.

♻️ Suggested simplification
 def test_inherited_flags_propagates_log_level_to_both() -> None:
     args = _ns(log_level="DEBUG")
     train_flags, chat_flags = _build_quickstart_inherited_flags(args)
-    assert ["--log-level", "DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")]
-    assert ["--log-level", "DEBUG"] == [flag for flag in chat_flags if flag in ("--log-level", "DEBUG")]
-    # Also assert the pair appears contiguously in both lists.
+    # Assert the --log-level/DEBUG pair appears contiguously in both lists.
     assert "--log-level" in train_flags
     assert train_flags[train_flags.index("--log-level") + 1] == "DEBUG"
     assert "--log-level" in chat_flags
     assert chat_flags[chat_flags.index("--log-level") + 1] == "DEBUG"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli_helpers.py` around lines 35 - 44, Remove the redundant
membership-comprehension assertions in
test_inherited_flags_propagates_log_level_to_both that check ["--log-level",
"DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")]
and the equivalent for chat_flags; they are vacuous and duplicate coverage
already provided by the contiguous-pair checks. Keep the calls to _ns and
_build_quickstart_inherited_flags and retain the explicit contiguous assertions
that verify train_flags and chat_flags contain "--log-level" followed
immediately by "DEBUG" (using train_flags.index("--log-level") + 1 and
chat_flags.index("--log-level") + 1). Ensure no other assertions are removed so
the test still validates adjacency and presence.
forgelm/trainer.py (2)

99-120: Minor: zip(..., strict=False) will silently drop completions when gold_answer length differs.

If TRL's per-sample column passthrough ever yields a gold_answer list shorter than completions (custom data collator, prompt-only fallback, etc.), the trailing completions silently get no reward signal instead of surfacing the mismatch. Given the contract is supposed to be aligned, strict=True would catch wiring regressions earlier without changing happy-path behavior.

♻️ Suggested change
-    for completion, gold in zip(completions, golds, strict=False):
+    for completion, gold in zip(completions, golds, strict=True):

Worth confirming TRL always passes per-sample columns aligned with completions. If it doesn't (e.g., for prompts without a gold_answer cell), strict=True would raise — in which case the current strict=False is the right call and this comment can be ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/trainer.py` around lines 99 - 120, In _math_reward_fn the
zip(completions, golds, strict=False) call can silently drop completions when
gold_answer is shorter; change it to zip(completions, golds, strict=True) inside
_math_reward_fn so mismatched lengths raise immediately and surface wiring
regressions (keep the rest of the logic using _ANSWER_PATTERN, _normalize_answer
and _answers_match unchanged); if there is a valid case where golds may be
missing for some completions, instead explicitly handle that case before zipping
(e.g., fill missing golds or branch) rather than relying on strict=False.

45-81: Optional: bare "m" in _REWARD_STRIP_TOKENS can over-strip non-unit suffixes.

The bare "m" token combined with endswith/startswith will strip a trailing m from any answer string that happens to end with one (e.g., a hypothetical "Answer: them" collapses to "the"). It's safe for the curated grpo-math seed dataset but brittle if a future template reuses this normalizer with less constrained outputs.

Consider gating unit stripping behind a word-boundary or whitespace check (e.g., only strip when preceded by whitespace or a digit) so the helper can be reused safely outside the math template. Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/trainer.py` around lines 45 - 81, The token list _REWARD_STRIP_TOKENS
and function _normalize_answer can over-strip the bare "m" from arbitrary
strings; change the stripping logic to only remove "m" when it appears as a unit
token at a word boundary (e.g., preceded/followed by whitespace or a digit)
rather than any suffix/prefix match. Update _normalize_answer to check
surrounding characters (or use a simple regex word-boundary check) before
trimming a unit token like "m" so you still strip genuine units (e.g., "5 m" or
"5m") but avoid trimming letters inside words like "them"; keep the existing
two-pass loop and other tokens unchanged and only add the boundary check for
single-letter unit tokens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@forgelm/cli.py`:
- Around line 831-848: The missing-TEMPLATE branch currently logs an error to
stderr and exits directly; update it to mirror the run_quickstart exception path
by checking the same output_format variable and, if output_format == "json",
print a JSON error envelope (e.g., {"success": False, "error": "<message>"} ) to
stdout instead of using logger.error, then call sys.exit(EXIT_CONFIG_ERROR);
modify the args.template check near the top of the quickstart invocation so it
uses the output_format-aware behavior, keeping the same error message text when
not in JSON mode.

In `@forgelm/templates/grpo-math/config.yaml`:
- Around line 46-47: Update the inline comment that currently says "Renamed from
grpo_max_new_tokens in v0.4.6" to reflect the shipped Quickstart Layer version
(v0.4.5) or make it version-agnostic; specifically edit the comment immediately
above the grpo_max_completion_length field (which replaced grpo_max_new_tokens)
to read either "Renamed from grpo_max_new_tokens in v0.4.5" or "Renamed from
grpo_max_new_tokens (v0.4.5+)" so the note matches the package/versioning in
pyproject.toml and docs/roadmap.md.

In `@tests/test_quickstart_subprocess.py`:
- Around line 105-159: The assertions that check for absolute paths use
POSIX-specific startswith("/") and will fail on Windows; update the two checks
(the cfg_value check in test_quickstart_subprocess.py and the model_path check
in test_chat_subprocess_uses_absolute_model_path) to use a platform-agnostic
absolute-path test such as os.path.isabs(cfg_value) or
pathlib.Path(cfg_value).is_absolute() (and similarly for model_path) so the
tests validate "absolute" on all OSes while still keeping the existing endswith
checks for the filename.

---

Duplicate comments:
In `@forgelm/quickstart.py`:
- Around line 367-447: The quickstart function currently performs filesystem
side effects even when dry_run=True; change it to short-circuit before any
on-disk work: after deciding chosen_model/selection_reason, if dry_run is True
skip calling _append_audit_event, skip creating config_target.parent, skip
_resolve_dataset and _materialize_config and the YAML write (and do not perform
the dataset copy), and return a QuickstartResult that reflects the
chosen_model/selection_reason with config_path and dataset_path set to None (or
a clearly non-written marker) and started_training=False; update any logfile
message to reflect dry-run state. Reference symbols: quickstart,
_append_audit_event, config_target, _resolve_dataset, _materialize_config,
QuickstartResult, and the YAML write block.

---

Nitpick comments:
In `@forgelm/trainer.py`:
- Around line 99-120: In _math_reward_fn the zip(completions, golds,
strict=False) call can silently drop completions when gold_answer is shorter;
change it to zip(completions, golds, strict=True) inside _math_reward_fn so
mismatched lengths raise immediately and surface wiring regressions (keep the
rest of the logic using _ANSWER_PATTERN, _normalize_answer and _answers_match
unchanged); if there is a valid case where golds may be missing for some
completions, instead explicitly handle that case before zipping (e.g., fill
missing golds or branch) rather than relying on strict=False.
- Around line 45-81: The token list _REWARD_STRIP_TOKENS and function
_normalize_answer can over-strip the bare "m" from arbitrary strings; change the
stripping logic to only remove "m" when it appears as a unit token at a word
boundary (e.g., preceded/followed by whitespace or a digit) rather than any
suffix/prefix match. Update _normalize_answer to check surrounding characters
(or use a simple regex word-boundary check) before trimming a unit token like
"m" so you still strip genuine units (e.g., "5 m" or "5m") but avoid trimming
letters inside words like "them"; keep the existing two-pass loop and other
tokens unchanged and only add the boundary check for single-letter unit tokens.

In `@forgelm/wizard.py`:
- Around line 326-378: The function _resolve_byod_dataset_path currently takes
an unused parameter template_name; remove this unused parameter from the
function signature and its docstring and update every caller to stop passing
template_name (or, alternatively, if you prefer to keep the context, thread
template_name into the user-facing strings—e.g., include it in the prompt and
error messages inside _resolve_byod_dataset_path—so the parameter is actually
used). Ensure you update the function definition, adjust all invocations to
match the new signature (or keep the same calls if you choose to use the
parameter), and revise the docstring to reflect the final behavior.

In `@tests/test_cli_helpers.py`:
- Around line 35-44: Remove the redundant membership-comprehension assertions in
test_inherited_flags_propagates_log_level_to_both that check ["--log-level",
"DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")]
and the equivalent for chat_flags; they are vacuous and duplicate coverage
already provided by the contiguous-pair checks. Keep the calls to _ns and
_build_quickstart_inherited_flags and retain the explicit contiguous assertions
that verify train_flags and chat_flags contain "--log-level" followed
immediately by "DEBUG" (using train_flags.index("--log-level") + 1 and
chat_flags.index("--log-level") + 1). Ensure no other assertions are removed so
the test still validates adjacency and presence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80595dc0-309c-476f-91cc-8faa22169936

📥 Commits

Reviewing files that changed from the base of the PR and between 02554cf and 56351b9.

📒 Files selected for processing (21)
  • .github/workflows/nightly.yml
  • config_template.yaml
  • docs/guides/alignment.md
  • docs/reference/configuration.md
  • docs/roadmap-tr.md
  • docs/roadmap.md
  • docs/roadmap/phase-12-quickstart.md
  • forgelm/cli.py
  • forgelm/config.py
  • forgelm/grpo_rewards.py
  • forgelm/quickstart.py
  • forgelm/templates/grpo-math/config.yaml
  • forgelm/trainer.py
  • forgelm/wizard.py
  • tests/test_alignment.py
  • tests/test_cli_helpers.py
  • tests/test_grpo_math_reward.py
  • tests/test_packaging.py
  • tests/test_quickstart_compat.py
  • tests/test_quickstart_subprocess.py
  • tests/test_trainer_grpo_config.py
✅ Files skipped from review due to trivial changes (2)
  • config_template.yaml
  • docs/reference/configuration.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/nightly.yml

Comment thread forgelm/cli.py Outdated
Comment thread forgelm/templates/grpo-math/config.yaml Outdated
Comment thread tests/test_quickstart_subprocess.py
…ict zip + 'm' boundary, version note, dead nits

Seven small findings, each verified against current code before fixing:

CLI / template error path consistency (forgelm/cli.py):
- The missing-TEMPLATE branch in _run_quickstart_cmd used logger.error
  + sys.exit. The run_quickstart exception handler immediately above it
  emits a JSON envelope when output_format == "json". Aligned both
  branches on the same shape so machine-readable callers always get a
  parsable JSON error, not a stderr log line.

Cross-platform absolute-path test (tests/test_quickstart_subprocess.py):
- The two argv-absolute-path assertions used .startswith("/") which
  fails on Windows where absolute paths look like "C:\\...". Switched
  to os.path.isabs() so the test validates the property on every OS
  while keeping the trailing-name endswith() checks.

Reward correctness (forgelm/trainer.py):
- _math_reward_fn used zip(..., strict=False), which silently truncates
  to the shorter of completions / gold_answer. A wiring regression
  (mismatched batch sizes) would surface as low reward instead of an
  exception. Switched to strict=True with a defensive early-return
  when gold_answer is missing entirely (the trainer wires this fn only
  when the dataset has gold_answer; the early-return is belt-and-suspenders).
- _normalize_answer's _REWARD_STRIP_TOKENS list contains bare "m" for
  meters, but with no boundary check it would shave the trailing "m"
  off ordinary words like "them" or "method". Added a boundary check
  for single-letter alpha tokens: strip only when the adjacent char is
  a digit or whitespace. Multi-char and non-alpha tokens (kg, km/h, $)
  are unchanged.

Template version note (forgelm/templates/grpo-math/config.yaml):
- The inline "Renamed from grpo_max_new_tokens in v0.4.6" comment was
  off-by-one; the rename actually shipped in v0.4.5. Corrected.

Wizard helper signature (forgelm/wizard.py):
- _resolve_byod_dataset_path took an unused template_name parameter.
  The function never references it; user-facing strings are generic
  "this template" wording. Removed the parameter and updated the caller.

Test cleanup (tests/test_cli_helpers.py):
- test_inherited_flags_propagates_log_level_to_both had a vacuous
  list-comprehension equality check that duplicated the contiguous-pair
  check below it. Removed; the contiguous-pair assertion is the real
  contract.

New regression tests (tests/test_grpo_math_reward.py):
- 7 parametrized cases for the "m" word-boundary rule (them / method /
  metric ton must NOT strip; "5 m" / "5m" / "m 5" / "m5" MUST strip).
- test_missing_gold_answer_kwarg_returns_zeros (defensive early-return).
- test_mismatched_lengths_raises (strict=True surfaces the regression).

Skipped finding: "dry_run inert" (per-run dataset materialization).
This was rejected in a prior round and the rationale stands — the
existing test test_quickstart_dry_run_writes_yaml_and_exits_clean
asserts the config file IS written on dry_run, and the CLI message
"To start training: forgelm --config ..." promises the operator that
the file is on disk and ready. Changing dry_run to a pure-preview mode
would break both. If a true preview mode is needed it should be a
separate flag (e.g. --preview), not a dry_run semantic shift.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
forgelm/trainer.py (1)

162-180: Heads-up: bool(first["gold_answer"]) treats falsy column values as "no gold".

For string golds (the bundled templates), this is fine and test_empty_gold_answer_treated_as_missing even pins it. But if a future dataset stores numeric golds where the first row happens to be 0 (or 0.0, False, None), the math reward gets silently disabled even though the column exists. Since the rest of the pipeline normalizes everything via _normalize_answer(gold) and string-equality, you might prefer probing for column presence only:

♻️ Optional: presence-based probe
-        first = train[0]
-        if isinstance(first, dict):
-            return "gold_answer" in first and bool(first["gold_answer"])
+        first = train[0]
+        if isinstance(first, dict):
+            return "gold_answer" in first

You'd need to update test_empty_gold_answer_treated_as_missing accordingly, or keep the current "empty string = missing" semantics if that's the deliberate UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/trainer.py` around lines 162 - 180, The detection in
_dataset_has_gold_answers treats falsy gold values (e.g., 0, 0.0, False, None)
as "no gold" by using bool(first["gold_answer"]); change the probe to check for
key presence instead (e.g., if isinstance(first, dict): return "gold_answer" in
first) so existence is detected regardless of value, and then update or adjust
test_empty_gold_answer_treated_as_missing to reflect the new presence-based
semantics (or explicitly special-case empty-string-as-missing if you want to
keep the current UX).
forgelm/cli.py (1)

798-921: Dispatcher logic is solid — the JSON-mode missing-template error, abs-path argv, inherited flags, and YAML-driven final_model_dir lookup all line up with the prior review feedback.

SonarCloud reports cognitive complexity 19 (>15) on this function. Optional split: pull the train-and-chat tail (lines 876–921) into a _run_quickstart_train_then_chat(args, result, output_format) helper if you want to silence it without behavior change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/cli.py` around lines 798 - 921, Function _run_quickstart has high
cognitive complexity; extract the training-and-chat tail into a new helper to
simplify the dispatcher. Create a helper named
_run_quickstart_train_then_chat(args, result, output_format) that contains
everything after the dry_run check starting at the subprocess training
invocation through the final sys.exit(EXIT_SUCCESS), move the subprocess logic
(train_cmd, subprocess.run, train_rc handling), the args.no_chat check, the
_load_quickstart_train_paths/final_model_dir check, and the chat subprocess
logic into that helper, and call it from _run_quickstart with the same arguments
so behavior, exit codes, logging, and inherited flags (from
_build_quickstart_inherited_flags) remain identical; ensure you import
subprocess in the helper or keep the existing import and preserve the same
checks/comments and return/exit semantics.
tests/test_quickstart_subprocess.py (1)

121-160: Patching pathlib.Path.is_dir globally is broad but acceptable here — the assertions on argv shape and absolute-path semantics remain precise.

Worth noting if this test starts becoming flaky as the quickstart codepath grows: a more surgical patch.object(Path, "is_dir", ...) scoped to a constructed instance, or a real final_model directory under tmp_path, would reduce the blast radius. Not a blocker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_quickstart_subprocess.py` around lines 121 - 160, The test
test_chat_subprocess_uses_absolute_model_path currently patches
pathlib.Path.is_dir globally via patch("pathlib.Path.is_dir",
return_value=True); to reduce blast radius and make the test less brittle,
change that to a more surgical approach by patching the Path class method via
patch.object(Path, "is_dir", return_value=True) or (alternatively) create an
actual final_model directory under tmp_path and point the patched
_load_quickstart_train_paths to that tmp_path-based directory; update references
in the test (the patch call and the mocked return_value for
_load_quickstart_train_paths used by main) so the Path.is_dir override is scoped
to Path and/or the created tmp_path/final_model instance rather than globally
patching pathlib.Path.is_dir.
forgelm/wizard.py (1)

326-378: Path resolution doesn't match the docstring — .expanduser() alone keeps relative paths relative.

Lines 329–330 promise "local paths are returned as resolved absolute paths," but line 360 only does Path(dataset_path).expanduser(). A user input of ./mydata.jsonl is validated and then returned as mydata.jsonl — still relative. Today the train subprocess inherits the parent cwd so this works by accident, but anything that later changes cwd between argv build and exec (or any non-cli consumer of the generated YAML) will silently miss the dataset.

Either tighten the impl to actually return absolute, or weaken the docstring.

♻️ Suggested fix (resolve to absolute, matching the docstring)
-        resolved = Path(dataset_path).expanduser()
+        resolved = Path(dataset_path).expanduser().resolve()
         if not resolved.is_file():
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/wizard.py` around lines 326 - 378, The docstring promises absolute
paths but the code only calls Path(dataset_path).expanduser() and returns that
(still relative); update _resolve_byod_dataset_path so after expanding user (~)
you check existence on that Path, then call resolved = resolved.resolve() (or
resolved.resolve(strict=True)) to convert it to an absolute path before
returning; i.e., change the block around resolved = Path(...).expanduser() to
first validate resolved.is_file(), then replace the returned value with
str(resolved.resolve()) so callers always get an absolute path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@forgelm/cli.py`:
- Around line 893-895: The code currently propagates the raw child return code
(train_rc) via logger.error and sys.exit, which can leak signal-derived codes
(e.g., 137, 139); change the logic around the train_rc check so that if train_rc
!= 0 you log the code but map/clamp any value outside the allowed contract
(0,1,2,3,4) to the canonical EXIT_TRAINING_ERROR constant (likely named
EXIT_TRAINING_ERROR or value 2) before calling sys.exit; update the block that
references train_rc, logger.error and sys.exit to compute an exit_code =
train_rc if train_rc in (0,1,2,3,4) else EXIT_TRAINING_ERROR, log accordingly,
and call sys.exit(exit_code).

---

Nitpick comments:
In `@forgelm/cli.py`:
- Around line 798-921: Function _run_quickstart has high cognitive complexity;
extract the training-and-chat tail into a new helper to simplify the dispatcher.
Create a helper named _run_quickstart_train_then_chat(args, result,
output_format) that contains everything after the dry_run check starting at the
subprocess training invocation through the final sys.exit(EXIT_SUCCESS), move
the subprocess logic (train_cmd, subprocess.run, train_rc handling), the
args.no_chat check, the _load_quickstart_train_paths/final_model_dir check, and
the chat subprocess logic into that helper, and call it from _run_quickstart
with the same arguments so behavior, exit codes, logging, and inherited flags
(from _build_quickstart_inherited_flags) remain identical; ensure you import
subprocess in the helper or keep the existing import and preserve the same
checks/comments and return/exit semantics.

In `@forgelm/trainer.py`:
- Around line 162-180: The detection in _dataset_has_gold_answers treats falsy
gold values (e.g., 0, 0.0, False, None) as "no gold" by using
bool(first["gold_answer"]); change the probe to check for key presence instead
(e.g., if isinstance(first, dict): return "gold_answer" in first) so existence
is detected regardless of value, and then update or adjust
test_empty_gold_answer_treated_as_missing to reflect the new presence-based
semantics (or explicitly special-case empty-string-as-missing if you want to
keep the current UX).

In `@forgelm/wizard.py`:
- Around line 326-378: The docstring promises absolute paths but the code only
calls Path(dataset_path).expanduser() and returns that (still relative); update
_resolve_byod_dataset_path so after expanding user (~) you check existence on
that Path, then call resolved = resolved.resolve() (or
resolved.resolve(strict=True)) to convert it to an absolute path before
returning; i.e., change the block around resolved = Path(...).expanduser() to
first validate resolved.is_file(), then replace the returned value with
str(resolved.resolve()) so callers always get an absolute path.

In `@tests/test_quickstart_subprocess.py`:
- Around line 121-160: The test test_chat_subprocess_uses_absolute_model_path
currently patches pathlib.Path.is_dir globally via patch("pathlib.Path.is_dir",
return_value=True); to reduce blast radius and make the test less brittle,
change that to a more surgical approach by patching the Path class method via
patch.object(Path, "is_dir", return_value=True) or (alternatively) create an
actual final_model directory under tmp_path and point the patched
_load_quickstart_train_paths to that tmp_path-based directory; update references
in the test (the patch call and the mocked return_value for
_load_quickstart_train_paths used by main) so the Path.is_dir override is scoped
to Path and/or the created tmp_path/final_model instance rather than globally
patching pathlib.Path.is_dir.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efae9881-392d-4e08-9828-7f6c07d08d7e

📥 Commits

Reviewing files that changed from the base of the PR and between 56351b9 and 2b0df78.

📒 Files selected for processing (7)
  • forgelm/cli.py
  • forgelm/templates/grpo-math/config.yaml
  • forgelm/trainer.py
  • forgelm/wizard.py
  • tests/test_cli_helpers.py
  • tests/test_grpo_math_reward.py
  • tests/test_quickstart_subprocess.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_cli_helpers.py
  • forgelm/templates/grpo-math/config.yaml

Comment thread forgelm/cli.py Outdated
…ex, float-eq, type hints

Thirteen findings from a SonarCloud scan, all verified against current
code before fixing.

Constants for repeated literals:
- forgelm/cli.py: "forgelm.cli" appeared 3x (logger name + two `python -m`
  argv calls). Hoisted to a module-level _CLI_MODULE constant.
- forgelm/quickstart.py: "Qwen/Qwen2.5-7B-Instruct" appeared 3x in the
  TEMPLATES registry (customer-support / domain-expert / medical-qa-tr).
  "HuggingFaceTB/SmolLM2-1.7B-Instruct" appeared 2x. Both hoisted to
  _QWEN_25_7B_INSTRUCT and _SMOLLM2_17B_INSTRUCT so a future model bump
  is one edit instead of N.

Cognitive complexity reductions:
- forgelm/cli.py:_run_quickstart_cmd was 19 / allowed 15. Extracted four
  helpers: _emit_quickstart_list, _emit_quickstart_result,
  _run_quickstart_train_subprocess, _run_quickstart_chat_subprocess.
  The body now reads as a linear dispatch.
- forgelm/trainer.py:_build_trainer was 16 / allowed 15. Extracted
  _build_simple_trl_trainer (covers SFT/ORPO/DPO/SimPO/KTO),
  _build_grpo_trainer, _resolve_grpo_reward_funcs, and a static
  _build_classifier_reward. _build_trainer is now a 2-line dispatch.
- forgelm/wizard.py:_resolve_byod_dataset_path was 18 / allowed 15.
  Extracted _validate_local_jsonl helper for the path-existence /
  first-line-JSONL parse block; the loop body is now flat.
- forgelm/wizard.py:run_wizard was 18 / allowed 15. Split into
  _finalize_quickstart_path (the "start now / save for later" prompt),
  _detect_hardware_and_backend (Step 1), and _run_full_wizard (Steps
  2-8). run_wizard itself is a 5-line dispatch.

Regex correctness (forgelm/trainer.py:_ANSWER_PATTERN):
- The previous pattern used a reluctant ``(.+?)`` with a triple-alt
  lookahead including a bare ``$``. SonarCloud flagged the reluctant
  quantifier as degenerating to "match 1 repetition" because the ``$``
  branch fires at every position the engine reaches. Switched to a
  greedy character-class form
  ``([^\s.!?\n][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)``: a non-space
  start, then any non-newline that isn't sentence punctuation, with
  bare "." between digits preserved via a negative lookahead.
  Round-tripped against all 15 known cases (decimals, units, time
  formats, fractions, prose-after-answer, etc.) before commit.

Float-equality assertions (Sonar: "Do not perform equality checks with
floating point values."):
- tests/test_grpo_format_reward.py L39, L40, L80, L81: switched to
  pytest.approx() — these were comparing exact 0.0 / 1.0 from arithmetic
  results that could in principle drift on different floating-point
  paths, even though the current code uses pure Python integer ratios.
- tests/test_quickstart_compat.py L156: same fix for an event field
  comparison.

Type-hint correctness:
- forgelm/trainer.py:_normalize_answer was annotated ``s: str`` but the
  body started with ``if s is None: return ""`` and tests pass None.
  Annotated as ``Optional[str]`` so the type signature matches the
  documented defensive behavior.

No behavior changes; all 638 tests still pass; quickstart smoke for
customer-support → dry-run round-trips through the trainer cleanly.
…path, surgical patches

Four findings, all verified against current code before fixing.

CLI exit-code contract (forgelm/cli.py):
- _run_quickstart_train_subprocess used to propagate the child's raw
  return code via sys.exit(train_rc). On SIGKILL the OS hands back 137,
  on SIGSEGV 139 — neither is part of ForgeLM's documented exit-code
  contract (0/1/2/3/4). Added a _PUBLIC_EXIT_CODES frozenset and a clamp:
  any rc outside the contract is mapped to EXIT_TRAINING_ERROR (2). The
  raw code still gets logged for debuggability.
- _run_quickstart_cmd's tail (training subprocess + no-chat short-circuit
  + chat subprocess + final exit) was extracted into a new
  _run_quickstart_train_then_chat helper. The dispatcher is now a flat
  sequence of seven steps with cognitive complexity well below the
  Sonar threshold.

Gold-answer presence detection (forgelm/trainer.py):
- _dataset_has_gold_answers used `bool(first["gold_answer"])` to gate
  presence, which silently treats `0` / `0.0` / `False` as "no gold
  answer". A math problem with the literal answer 0 ("What is 5 - 5?")
  would have its correctness reward dropped on the floor.
- Switched to presence-based detection with a narrow exception for
  empty-string-as-placeholder: `"gold_answer" in first` and `val is not
  None and val != ""`. Matches the docstring's "schema placeholder vs.
  real label" semantics.

Wizard absolute-path contract (forgelm/wizard.py):
- _validate_local_jsonl's docstring promised absolute paths but only
  called Path(...).expanduser() — relative inputs like "./data/foo.jsonl"
  came back relative. Added Path.resolve() so callers downstream can
  cd freely without losing the dataset.

Test surgical patching (tests/test_quickstart_subprocess.py):
- test_chat_subprocess_uses_absolute_model_path was patching
  pathlib.Path.is_dir globally. That overrode every Path.is_dir() call
  in the importlib / yaml / setuptools chain that ran during the test —
  a foot-gun any time a parallel agent added a new is_dir() probe.
- Replaced with a real tmp_path/checkpoints/final_model directory
  pointed at by the patched _load_quickstart_train_paths. Same
  assertions; no global mutations.

Tests:
- tests/test_grpo_math_reward.py — three new presence tests
  (test_zero_int_, test_zero_float_, test_false_gold_answer_treated_as_present);
  test_empty_gold_answer_treated_as_missing extended docstring; new
  test_none_gold_answer_treated_as_missing for the None-as-placeholder
  case.
- tests/test_cli_quickstart_wiring.py — TestTrainExitCodeClamping with
  test_signal_derived_rc_clamped_to_training_error (rc=139 → 2) and
  test_documented_rc_propagates_unchanged (rc=3 stays 3).

644 passed (+6 new), smoke clean, no behavior regression for the
existing happy paths.
@cemililik

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
forgelm/quickstart.py (1)

401-433: ⚠️ Potential issue | 🟡 Minor

Reminder: dry_run=True still materializes the dataset and writes config.

run_quickstart(..., dry_run=True) still calls config_target.parent.mkdir(...) and _resolve_dataset(...) (which copies the seed JSONL). The flag effectively only flips a field on QuickstartResult for the CLI to consult later — the docstring at line 384 ("Generate the YAML without invoking training") suggests no on-disk side effects beyond the YAML.

This was raised in a prior review and not yet addressed; flagging again so it isn't lost. Either tighten the docstring or skip dataset materialization on dry_run=True.

🧹 Nitpick comments (2)
forgelm/trainer.py (1)

47-50: Redundant \n inside the negated \s class.

SonarCloud is right here: in [^\s.!?\n], \s already covers \n (along with \t, \r, etc.), so the trailing \n is dead. The second class [^.!?\n] does need the explicit \n because there's no \s there — keep that one.

♻️ Drop the duplicate
 _ANSWER_PATTERN = re.compile(
-    r"answer\s*:\s*([^\s.!?\n][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)",
+    r"answer\s*:\s*([^\s.!?][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)",
     re.IGNORECASE,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/trainer.py` around lines 47 - 50, The regex in _ANSWER_PATTERN
contains a redundant "\n" inside the first negated character class [^\s.!?\n];
remove the "\n" from that class so it becomes [^\s.!?] while leaving the second
class [^.!?\n] unchanged; update the _ANSWER_PATTERN definition accordingly to
keep semantics identical but eliminate the duplicate newline exclusion.
tests/test_grpo_math_reward.py (1)

116-229: Consider adding a non-string gold_answer case to TestMathRewardFn.

TestDatasetHasGoldAnswers covers 0, 0.0, and False as present labels, but every case in TestMathRewardFn uses string gold_answer (e.g. ["7"], ["2/5"]). That gap is what hides the _normalize_answer AttributeError flagged in forgelm/trainer.py — adding a single parametrized test like _math_reward_fn(["Answer: 0"], gold_answer=[0]) would catch it and lock in the contract that the gating function advertises.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_grpo_math_reward.py` around lines 116 - 229, Add a test to
TestMathRewardFn that supplies a non-string gold_answer to exercise type
handling (e.g., call _math_reward_fn(["Answer: 0"], gold_answer=[0]) and assert
reward 1.0), so the reward pipeline (_math_reward_fn and underlying
_normalize_answer in forgelm/trainer.py) handles numeric/boolean labels without
raising AttributeError; add this as a new unit (or parametrize existing tests)
alongside existing string cases to lock in the contract that gold_answer may be
non-string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@forgelm/cli.py`:
- Around line 940-953: The except block around run_quickstart currently catches
(FileNotFoundError, ValueError) but not FileExistsError raised by
forgelm.quickstart._resolve_dataset; update the error handling in the try/except
that invokes run_quickstart to also catch FileExistsError (either add it to the
tuple or add a separate except FileExistsError as e), and ensure the handler
uses the same JSON/text reporting logic (printing {"success": False, "error":
str(e)} for output_format == "json" and logger.error("Quickstart failed: %s", e)
otherwise) before calling sys.exit(EXIT_CONFIG_ERROR).

In `@forgelm/trainer.py`:
- Around line 101-123: _normalize_answer currently calls s.strip() and will
crash for non-string gold values (ints/bools) which _dataset_has_gold_answers
allows; to fix, coerce non-None inputs to str at the top of _normalize_answer
(e.g., treat None as "" and otherwise replace s with str(s)) so subsequent
.strip/.rstrip calls are safe, and add a regression test in TestMathRewardFn
that calls _math_reward_fn(["Answer: 0"], gold_answer=[0]) and asserts rewards
== [1.0] (also include a boolean case like gold_answer=[False]) to ensure
numeric/bool gold labels don't crash.

In `@forgelm/wizard.py`:
- Around line 369-389: The HF Hub regex branch misclassifies relative local
paths (like "data/train.jsonl"); change the control flow in the input loop in
wizard.py so you validate the path as a local JSONL first (call
_validate_local_jsonl(dataset_path) immediately after basic checks) and only if
that returns None then test _HF_HUB_ID_RE.match(dataset_path) and treat it as a
Hub ID; keep the existing messages but ensure _validate_local_jsonl is attempted
before the _HF_HUB_ID_RE check so local files are accepted and Hub IDs are a
fallback.

---

Nitpick comments:
In `@forgelm/trainer.py`:
- Around line 47-50: The regex in _ANSWER_PATTERN contains a redundant "\n"
inside the first negated character class [^\s.!?\n]; remove the "\n" from that
class so it becomes [^\s.!?] while leaving the second class [^.!?\n] unchanged;
update the _ANSWER_PATTERN definition accordingly to keep semantics identical
but eliminate the duplicate newline exclusion.

In `@tests/test_grpo_math_reward.py`:
- Around line 116-229: Add a test to TestMathRewardFn that supplies a non-string
gold_answer to exercise type handling (e.g., call _math_reward_fn(["Answer: 0"],
gold_answer=[0]) and assert reward 1.0), so the reward pipeline (_math_reward_fn
and underlying _normalize_answer in forgelm/trainer.py) handles numeric/boolean
labels without raising AttributeError; add this as a new unit (or parametrize
existing tests) alongside existing string cases to lock in the contract that
gold_answer may be non-string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d63ae65a-270a-42c1-bab9-b9e05ffe1acb

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0df78 and dfeb6e5.

📒 Files selected for processing (9)
  • forgelm/cli.py
  • forgelm/quickstart.py
  • forgelm/trainer.py
  • forgelm/wizard.py
  • tests/test_cli_quickstart_wiring.py
  • tests/test_grpo_format_reward.py
  • tests/test_grpo_math_reward.py
  • tests/test_quickstart_compat.py
  • tests/test_quickstart_subprocess.py
✅ Files skipped from review due to trivial changes (1)
  • tests/test_cli_quickstart_wiring.py

Comment thread forgelm/cli.py
Comment thread forgelm/trainer.py Outdated
Comment thread forgelm/wizard.py Outdated
…ative-path misclassify, regex cleanup

Five findings, all verified against current code before fixing.
Distinct from the round-2 commit dfeb6e5 (exit-code clamp + cog
refactor); these touch different code paths.

cli.py — FileExistsError missing from quickstart catch tuple:
  run_quickstart's try/except caught (FileNotFoundError, ValueError) but
  not FileExistsError, which _resolve_dataset raises when the operator
  passes --output pointing at a directory that already has a seed
  dataset (the per-run dir collision guard). Without this, a re-run
  with an explicit --output emits a Python traceback instead of the
  actionable three-bullet message. Added FileExistsError to the tuple
  so the same JSON-or-text reporting path runs.

trainer.py — gold-answer presence vs truthiness:
  _dataset_has_gold_answers used `bool(first["gold_answer"])` which
  treats `0`, `0.0`, `False`, and `""` all as "no gold". A math
  problem with the correct answer `"0"` is a real and common case,
  so dropping it silently is wrong. Switched to presence-based
  detection: empty string and None still count as missing (those are
  schema-placeholder shapes), but `0`, `0.0`, `False` now count as
  real gold answers.

trainer.py — _normalize_answer crashed on non-string gold:
  Once the dataset probe accepts numeric/bool gold values, the reward
  function passes them straight to `_normalize_answer`, which called
  `.strip()` on the input and crashed on int/bool with AttributeError.
  Annotated the parameter as `Any` and added an `isinstance(s, str)`
  coercion at the top so int/float/bool stringify cleanly.

trainer.py — redundant `\n` in _ANSWER_PATTERN char class:
  The first negated class `[^\s.!?\n]` had `\n` inside it but `\s`
  already matches newlines. Dropped to `[^\s.!?]`. Pure cleanup,
  semantics unchanged.

wizard.py — relative local paths misclassified as HF Hub IDs:
  The HF Hub regex `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` matches paths
  like `data/train.jsonl` (single slash, safe-name char class).
  The previous loop tested Hub regex first, so the path silently
  bypassed JSONL parse validation and was sent to the trainer as a
  Hub ID. Reordered the loop: try local file first; only when the
  path doesn't exist on disk fall back to Hub semantics.
  Implementation uses a sentinel `_BYOD_LOCAL_NOT_FOUND` returned by
  `_validate_local_jsonl` to distinguish "exists-but-malformed"
  (re-prompt) from "not on disk" (Hub fallback).

Tests:
- tests/test_grpo_math_reward.py:
  * `test_non_string_gold_answer_does_not_crash` — parametrized over
    int 0, float 0.0, bool False, int 42, float 3.14.
- tests/test_wizard_byod.py:
  * `test_byod_relative_local_path_not_misclassified_as_hub_id` —
    regression for the local-vs-Hub ordering.
  * Updated `test_byod_expands_user_home` to compare via Path equality
    instead of string equality (macOS /tmp → /private/tmp symlink
    canonicalisation now applies after the .resolve() call inside
    _validate_local_jsonl).

650 passed / 8 skipped. Smoke confirmed: re-running quickstart with the
same --output now prints the FileExistsError message instead of a
traceback.
…ception reraise, notebook DRY

Six findings; verified line numbers had drifted across earlier rounds
but all six were genuine in current code.

forgelm/cli.py — _output_result cog complexity:
  Sonar pointed at line 527 (a stale snapshot from before the Phase 10.5
  refactor). The actual offending function in current code was
  _output_result: 6 conditional branches in the JSON envelope plus 5 in
  the text-mode path drove the metric to 39. Split into four helpers:
  _build_result_json_envelope, _log_result_status, _log_cost_summary,
  _log_benchmark_summary. Each helper handles one concern; _output_result
  is now a 5-line dispatch.

forgelm/fit_check.py — estimate_vram cog complexity:
  20 → ~10 by extracting three helpers: _resolve_quant_scheme (the 4-bit
  / 8-bit / dtype-map ladder), _component_breakdown (the LoRA-vs-GaLore
  branch and the per-component GB calculation), _detect_available_vram_gb
  (the torch.cuda.mem_get_info() probe with its exception fallback), and
  _verdict_for (the threshold ladder). _DTYPE_TO_QUANT moved to module
  scope so it isn't rebuilt on every call. estimate_vram body is now a
  linear sequence of named steps.

forgelm/judge.py — run_judge_evaluation cog complexity:
  19 → ~10 by extracting _score_eval_prompts (the per-prompt generation
  + judge-call + score-clip loop) and _summarize_judge_scores (the
  no-valid-scores vs avg-vs-min decision tree). The public function
  reads as input-gate → score → summarize → optional-save → return.

forgelm/merging.py — torch.quantile missing dim:
  Sonar wants an explicit dim argument. flat is already 1-D from
  .flatten(), so dim=0 produces the same scalar as the default; added
  it explicitly to satisfy the rule and document intent.

tests/runtime_smoke.py — exception wrapping:
  The smoke test caught SystemExit from main() and re-raised as
  RuntimeError, masking the CLI's exit-code contract. Sonar flagged
  this as "reraise this exception". Switched to bare ``raise`` so
  SystemExit propagates unchanged with its original exit code; the
  smoke runner sees the same non-zero code the CLI emitted.

notebooks/kto_binary_feedback.ipynb — duplicated literal:
  "What is Python?" appeared 3 times across two cells. Hoisted to
  PROMPT_PYTHON in Step 3 (where the dataset is built) and reused in
  Step 8's test_prompts list. Notebook cell ordering means Step 3
  always runs before Step 8 in a normal Run-All flow.

650 passed / 8 skipped. fit-check smoke verified estimate_vram still
produces an identical breakdown; quickstart smoke for customer-support
still round-trips through the trainer cleanly.
…aths

Two SonarCloud security findings.

forgelm/grpo_rewards.py — regex polynomial backtracking risk:
  The previous _ANSWER_PATTERN was ``answer\s*:\s*\S[^\n]*?\s*\Z`` —
  a reluctant ``[^\n]*?`` followed by ``\s*\Z``. The two quantifiers
  overlap (``\s`` is a subset of ``[^\n]``) which static analysis flags
  as a polynomial-runtime / catastrophic-backtracking surface; with a
  long input that almost-but-doesn't match the trailing ``Answer:``
  shape the engine could spend O(n²) walking the reluctant quantifier.

  Fix is two-part:
  1) The pattern is now a greedy ``answer\s*:\s*\S[^\n]*\Z`` — no
     reluctant quantifier, no overlap, end pinned by ``\Z``.
  2) The caller in format_match_reward() now strips trailing
     whitespace before passing the completion to .search(). That
     collapses what used to be the ``\s*\Z`` tail (the source of the
     overlap) to a plain ``\Z``.

  Linear-time guaranteed; existing semantic still preserved
  (case-insensitive "Answer:" anywhere, value at least one non-space
  char, optional trailing units up to end-of-string).

tests/test_quickstart_hardening.py — hardcoded /tmp paths:
  Two test_null_model_section_is_replaced_not_crashed assertions and
  one test_non_mapping_model_value_raises_clear_error call passed
  literal "/tmp/data.jsonl" as the dataset path. The path is never
  actually opened (it's only rendered into a YAML structure for
  comparison), but Sonar flags any string referring to a globally
  writable directory because the same pattern in production code
  would create a TOCTOU race against an attacker pre-creating the
  file. Switched all three call sites to ``str(tmp_path / "data.jsonl")``
  using the existing pytest tmp_path fixture — same shape, no
  publicly-writable path on the surface.

650 passed / 8 skipped. ReDoS micro-benchmark: 20 000-char worst-case
input matches in 0.07 ms with both old and new patterns (the old
form was theoretically O(n²) but Python's ``re`` engine memoises
many cases; the static analyser is what flags the shape, and the
shape is now linear).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
forgelm/judge.py (1)

308-312: ⚠️ Potential issue | 🟡 Minor

Non-numeric judge score values can crash the entire eval run.

float(raw_score) will raise ValueError/TypeError if the judge returns a non-numeric score (e.g., "score": "high", "score": "8/10", "score": [8]). Because this happens inside the per-prompt loop with no try/except, a single malformed verdict aborts the whole run_judge_evaluation and bypasses the failure_count / score=None sentinel path that the rest of the refactor carefully preserves.

🛡️ Suggested guard
-        raw_score = result.get("score")
-        score = _clip_judge_score(float(raw_score) if raw_score is not None else None)
+        raw_score = result.get("score")
+        try:
+            numeric_score = float(raw_score) if raw_score is not None else None
+        except (TypeError, ValueError):
+            logger.warning("Judge returned non-numeric score %r; treating as failure", raw_score)
+            numeric_score = None
+        score = _clip_judge_score(numeric_score)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/judge.py` around lines 308 - 312, When converting per-judge raw_score
to float inside run_judge_evaluation, guard the conversion to avoid crashing on
non-numeric values: wrap float(raw_score) in a try/except (catch ValueError and
TypeError), and on exception set score = None, increment failure_count, and
proceed to append score to scores (so the existing _clip_judge_score(None) /
sentinel path is preserved); use the same symbols raw_score, score,
_clip_judge_score, failure_count, scores and result to locate and update the
logic.
🧹 Nitpick comments (5)
forgelm/judge.py (1)

326-332: Minor: pass len(eval_prompts) instead of the full list.

_summarize_judge_scores only uses eval_prompts for its length in log/error messages. Passing an int (e.g., num_prompts) tightens the contract and avoids implying the function inspects prompt content.

♻️ Optional refactor
 def _summarize_judge_scores(
     *,
     scores: List[Optional[float]],
     failure_count: int,
-    eval_prompts: List[str],
+    num_prompts: int,
     min_score: float,
 ) -> tuple[float, bool, Optional[str]]:

…and update the two len(eval_prompts) references plus the call site at line 265.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/judge.py` around lines 326 - 332, Change _summarize_judge_scores so
it accepts num_prompts: int instead of eval_prompts: List[str] (update the
signature and type annotation) and replace its internal uses of
len(eval_prompts) with num_prompts in log/error messages; then update every call
that currently passes eval_prompts to pass len(eval_prompts)) instead (ensure
the call site(s) use the same symbol names and types). Ensure the return tuple
and behavior remain unchanged and update any docstring/comments referring to
eval_prompts.
tests/test_grpo_format_reward.py (1)

205-248: Classifier-reward wiring test is correct, but the assertion is weaker than the docstring claims.

The docstring on line 244 says "calling it should invoke our mock_rw_model exactly once," but line 248 only asserts mock_rw_model.called (boolean). Consider tightening to assert mock_rw_model.call_count == 1 to actually enforce the "exactly once" contract — otherwise a future regression that double-invokes the reward model per call would slip through.

♻️ Tighten the call-count assertion
-    assert mock_rw_model.called, "classifier-backed reward closure must call the loaded reward model"
+    assert mock_rw_model.call_count == 1, (
+        "classifier-backed reward closure must call the loaded reward model exactly once"
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_grpo_format_reward.py` around lines 205 - 248, The test
test_reward_func_uses_classifier_when_configured currently only asserts
mock_rw_model.called; change this to assert the exact call count to match the
docstring by replacing that boolean check with a precise check such as assert
mock_rw_model.call_count == 1 so the closure invocation is enforced to call the
loaded reward model exactly once (refer to mock_rw_model and reward_funcs in the
test for locating the assertion to update).
tests/test_quickstart_subprocess.py (1)

49-168: LGTM — covers the high-value subprocess argv invariants without over-coupling.

The actionable-recovery message check (line 64-65) and the os.path.isabs portability fix on lines 112/167 hit the right level of specificity. Materializing a real tmp_path/checkpoints/final_model instead of monkey-patching Path.is_dir() globally is the right call — the past foot-gun is gone.

Minor optional polish: the two main()-driven tests share ~80% of their setup (recorder, argv, patches, final_model_dir creation). A small fixture (e.g. _invoke_quickstart) would let each test focus on the one assertion that distinguishes them, though it isn't blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_quickstart_subprocess.py` around lines 49 - 168, Tests
test_train_subprocess_uses_absolute_config_path and
test_chat_subprocess_uses_absolute_model_path duplicate ~80% setup; extract a
small helper (e.g. _invoke_quickstart or fixture) that builds argv, creates
final_model_dir, constructs the _RunRecorder, applies the same patches
(forgelm.quickstart._detect_available_vram_gb,
forgelm.cli._load_quickstart_train_paths, subprocess.run, sys.argv), runs main()
and returns the recorder and exit code so each test only asserts its specific
argv details (use symbols _RunRecorder, main, and the two test names to locate
callers to refactor).
tests/test_cli_quickstart_wiring.py (1)

32-52: Optional: _RunRecorder is now duplicated verbatim with tests/test_quickstart_subprocess.py.

Both files explicitly note the duplication is intentional, but a tiny shared tests/conftest.py (or tests/_quickstart_helpers.py) holding _RunRecorder and the fake_train_paths/_invoke_main helpers would let future quickstart-wiring tests start with one import instead of copy-pasting the harness. Non-blocking — flag to revisit when a third file in the same shape lands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli_quickstart_wiring.py` around lines 32 - 52, Extract the
duplicated test helper class _RunRecorder and the helpers fake_train_paths and
_invoke_main into a single shared test helper module (e.g., a tests conftest or
helpers module) and update both test files to import them instead of
duplicating; specifically, move the _RunRecorder implementation (preserving its
constructor signature, __call__ behavior and internal _Completed class) and the
fake_train_paths/_invoke_main helper functions into the shared module, then
replace the verbatim copies in tests/test_cli_quickstart_wiring.py and
tests/test_quickstart_subprocess.py with imports from that shared module.
forgelm/fit_check.py (1)

253-256: Optional: the m.config.torch_dtype fallback is unreachable against the current ModelConfig schema.

ForgeConfig.model is a Pydantic ModelConfig with extra="forbid" and no torch_dtype (or config) field — so both getattr(m, "torch_dtype", None) and getattr(getattr(m, "config", None), "torch_dtype", None) always evaluate to None, and the function always falls through to the bf16 default. If that's intentional defensive code for a future field, fine; if you want to actually honor a user-declared dtype, the field needs to exist on ModelConfig first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/fit_check.py` around lines 253 - 256, The fallback to
m.config.torch_dtype in fit_check.py is unreachable because ForgeConfig.model
uses a Pydantic ModelConfig that lacks a torch_dtype (and config) field; add an
optional torch_dtype field to the ModelConfig schema (e.g., Optional[str] or
Optional[torch.dtype]) with appropriate validation/default so getattr(m,
"torch_dtype", None) returns the user-declared dtype, and keep the existing
lookup in fit_check.py unchanged; update the ModelConfig class definition to
include the new torch_dtype attribute and tests/docs where ModelConfig is
constructed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@forgelm/trainer.py`:
- Around line 633-651: The _build_classifier_reward function needs two fixes:
add a pad_token guard before tokenization by setting _rw_tok.pad_token =
_rw_tok.pad_token or _rw_tok.eos_token to avoid "asking to pad" errors for
tokenizers without an explicit pad token (e.g., GPT-2), and update the function
docstring to state the expected reward-head shape (e.g., "expects a
sequence-classification head with output_size=1; logits[:, 0] returns the scalar
reward; 2-class heads will yield class logits and are incompatible without
post-processing"). Make these changes inside the _build_classifier_reward
staticmethod (affecting symbols _rw_tok and _reward_fn and clarifying use of
logits[:, 0]).

---

Outside diff comments:
In `@forgelm/judge.py`:
- Around line 308-312: When converting per-judge raw_score to float inside
run_judge_evaluation, guard the conversion to avoid crashing on non-numeric
values: wrap float(raw_score) in a try/except (catch ValueError and TypeError),
and on exception set score = None, increment failure_count, and proceed to
append score to scores (so the existing _clip_judge_score(None) / sentinel path
is preserved); use the same symbols raw_score, score, _clip_judge_score,
failure_count, scores and result to locate and update the logic.

---

Nitpick comments:
In `@forgelm/fit_check.py`:
- Around line 253-256: The fallback to m.config.torch_dtype in fit_check.py is
unreachable because ForgeConfig.model uses a Pydantic ModelConfig that lacks a
torch_dtype (and config) field; add an optional torch_dtype field to the
ModelConfig schema (e.g., Optional[str] or Optional[torch.dtype]) with
appropriate validation/default so getattr(m, "torch_dtype", None) returns the
user-declared dtype, and keep the existing lookup in fit_check.py unchanged;
update the ModelConfig class definition to include the new torch_dtype attribute
and tests/docs where ModelConfig is constructed.

In `@forgelm/judge.py`:
- Around line 326-332: Change _summarize_judge_scores so it accepts num_prompts:
int instead of eval_prompts: List[str] (update the signature and type
annotation) and replace its internal uses of len(eval_prompts) with num_prompts
in log/error messages; then update every call that currently passes eval_prompts
to pass len(eval_prompts)) instead (ensure the call site(s) use the same symbol
names and types). Ensure the return tuple and behavior remain unchanged and
update any docstring/comments referring to eval_prompts.

In `@tests/test_cli_quickstart_wiring.py`:
- Around line 32-52: Extract the duplicated test helper class _RunRecorder and
the helpers fake_train_paths and _invoke_main into a single shared test helper
module (e.g., a tests conftest or helpers module) and update both test files to
import them instead of duplicating; specifically, move the _RunRecorder
implementation (preserving its constructor signature, __call__ behavior and
internal _Completed class) and the fake_train_paths/_invoke_main helper
functions into the shared module, then replace the verbatim copies in
tests/test_cli_quickstart_wiring.py and tests/test_quickstart_subprocess.py with
imports from that shared module.

In `@tests/test_grpo_format_reward.py`:
- Around line 205-248: The test test_reward_func_uses_classifier_when_configured
currently only asserts mock_rw_model.called; change this to assert the exact
call count to match the docstring by replacing that boolean check with a precise
check such as assert mock_rw_model.call_count == 1 so the closure invocation is
enforced to call the loaded reward model exactly once (refer to mock_rw_model
and reward_funcs in the test for locating the assertion to update).

In `@tests/test_quickstart_subprocess.py`:
- Around line 49-168: Tests test_train_subprocess_uses_absolute_config_path and
test_chat_subprocess_uses_absolute_model_path duplicate ~80% setup; extract a
small helper (e.g. _invoke_quickstart or fixture) that builds argv, creates
final_model_dir, constructs the _RunRecorder, applies the same patches
(forgelm.quickstart._detect_available_vram_gb,
forgelm.cli._load_quickstart_train_paths, subprocess.run, sys.argv), runs main()
and returns the recorder and exit code so each test only asserts its specific
argv details (use symbols _RunRecorder, main, and the two test names to locate
callers to refactor).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4cbe8d1a-2832-49ce-a694-d73acb3787a4

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0df78 and a933021.

📒 Files selected for processing (15)
  • forgelm/cli.py
  • forgelm/fit_check.py
  • forgelm/judge.py
  • forgelm/merging.py
  • forgelm/quickstart.py
  • forgelm/trainer.py
  • forgelm/wizard.py
  • notebooks/kto_binary_feedback.ipynb
  • tests/runtime_smoke.py
  • tests/test_cli_quickstart_wiring.py
  • tests/test_grpo_format_reward.py
  • tests/test_grpo_math_reward.py
  • tests/test_quickstart_compat.py
  • tests/test_quickstart_subprocess.py
  • tests/test_wizard_byod.py
✅ Files skipped from review due to trivial changes (1)
  • forgelm/merging.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_grpo_math_reward.py
  • tests/test_quickstart_compat.py
  • forgelm/wizard.py

Comment thread forgelm/trainer.py
Comment on lines +633 to +651
@staticmethod
def _build_classifier_reward(reward_model_path: str):
"""Wrap an HF sequence-classification model as a TRL reward callable."""
from transformers import AutoModelForSequenceClassification
from transformers import AutoTokenizer as _AutoTok

self.trainer = GRPOTrainer(**trainer_kwargs)
else:
raise ValueError(f"Unknown trainer_type: {tt}")
_rw_tok = _AutoTok.from_pretrained(reward_model_path)
_rw_model = AutoModelForSequenceClassification.from_pretrained(reward_model_path, device_map="auto")

def _reward_fn(completions, **kwargs):
import torch as _t

inputs = _rw_tok(completions, return_tensors="pt", truncation=True, padding=True, max_length=512)
inputs = {k: v.to(_rw_model.device) for k, v in inputs.items()}
with _t.no_grad():
logits = _rw_model(**inputs).logits
return logits[:, 0].tolist()

return _reward_fn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if accelerate is in install_requires / dependencies
fd -t f 'pyproject.toml|setup.py|setup.cfg|requirements*.txt' --max-depth 3 \
  --exec rg -n -i 'accelerate' {} \;

Repository: cemililik/ForgeLM

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Find and examine the dependency declaration files
fd -t f 'pyproject.toml|setup.py' --max-depth 3 -x cat -n {} \; | head -100

Repository: cemililik/ForgeLM

Length of output: 3589


🏁 Script executed:

#!/bin/bash
# Check for pad_token handling in the trainer.py file
rg -n 'pad_token' forgelm/trainer.py -A 2 -B 2

Repository: cemililik/ForgeLM

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look for any documentation or docstring mentioning reward model shape expectations
rg -n 'reward.*model|reward.*shape|scalar.*reward' forgelm/trainer.py -A 3 -B 1

Repository: cemililik/ForgeLM

Length of output: 2458


Add pad_token guard and document reward-model shape assumption in docstring.

The _build_classifier_reward method has two issues that should be addressed:

  1. _rw_tok(completions, ..., padding=True, ...) will raise ValueError: Asking to pad but the tokenizer does not have a padding token for tokenizers without an explicit padding token (e.g., some GPT-2-based reward models). Add _rw_tok.pad_token = _rw_tok.pad_token or _rw_tok.eos_token before tokenizing to handle this edge case.

  2. The docstring does not document that logits[:, 0] assumes a scalar (1-output) reward head. For 2-class classifiers, this returns the negative class logit. Document the expected reward-model shape (e.g., "expects a sequence-classification head with output_size=1") so users configure compatible models.

Note: accelerate>=0.27.0 is already a declared runtime dependency, so device_map="auto" support is guaranteed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/trainer.py` around lines 633 - 651, The _build_classifier_reward
function needs two fixes: add a pad_token guard before tokenization by setting
_rw_tok.pad_token = _rw_tok.pad_token or _rw_tok.eos_token to avoid "asking to
pad" errors for tokenizers without an explicit pad token (e.g., GPT-2), and
update the function docstring to state the expected reward-head shape (e.g.,
"expects a sequence-classification head with output_size=1; logits[:, 0] returns
the scalar reward; 2-class heads will yield class logits and are incompatible
without post-processing"). Make these changes inside the
_build_classifier_reward staticmethod (affecting symbols _rw_tok and _reward_fn
and clarifying use of logits[:, 0]).

…plicit rationale

Three Codacy annotations against current code; verified each and added
inline rationale comments rather than functional changes — the analyser
warnings are about shape, not behaviour, and the call sites are already
safe by construction.

forgelm/cli.py L884, L907 — subprocess.run without static string:
  Both call sites already carried `# noqa: S603 # nosec B603`, but
  Codacy's runner doesn't read those Bandit/ruff suppressions inline.
  Added a 6-line block comment above each subprocess.run call spelling
  out the security argument:
    - argv is a fixed list (not a shell string) → shell=False implicit.
    - argv[0] is sys.executable, not a user-controlled command name.
    - argv[1..3] are the literal "-m"/_CLI_MODULE/[--config|chat]; the
      only user-influenced segment is the abspath of the config or
      model dir, passed as a single argv element with no shell
      expansion or string concatenation.
  No injection surface — Codacy can flag this if the rule is purely
  syntactic, but reviewers reading the code now have a written argument
  for why the suppression is correct.

forgelm/quickstart.py L211 — "Possibly found usage of AI tooling: PyTorch":
  Codacy's ML-detection rule fires on `import torch`. Torch is a
  documented core dependency for the trainer (declared in pyproject.toml
  and the README), and the import is local *only* to keep
  `forgelm --help` startup torch-free. Added a multi-line rationale
  comment explaining why this is expected and an inline
  `# codacy-disable: pytorch-detector — core dep, lazy for cold-start cost`
  marker.

Tests still 650 passed / 8 skipped — pure comment / annotation changes,
no logic touched.
@sonarqubecloud

Copy link
Copy Markdown

@cemililik
cemililik merged commit 03999d0 into main Apr 26, 2026
9 of 10 checks passed
cemililik added a commit that referenced this pull request Apr 26, 2026
PR #9 landed Phase 10.5 on main (merge commit 03999d0), but the v0.4.5
git tag and PyPI upload have not happened yet — `git tag -l v0.4.5`
returns empty. The roadmap docs claimed 'Tagged but not yet on PyPI'
which over-reported the release state.

- docs/roadmap.md: latest-release block + doc map line clarified.
- docs/roadmap-tr.md: parallel Turkish wording.
- docs/roadmap/phase-12-quickstart.md: DONE banner now says
  'merged to main as v0.4.5 via PR #9 (...); tag + PyPI publish are
  the remaining release-engineering steps' instead of 'shipped in
  v0.4.5'.

No content/code changes — wording-only, gets the docs back in sync
with what git actually contains.
cemililik added a commit that referenced this pull request Apr 26, 2026
…reward, CI)

CHANGELOG.md, docs/roadmap/releases.md, docs/guides/quickstart.md were
written before two later changes landed:

- code-assistant fallback model bumped to Qwen2.5-Coder-1.5B-Instruct
  (was generic SmolLM2-1.7B-Instruct).
- medical-qa-tr fallback model bumped to Qwen2.5-1.5B-Instruct (was
  English-only SmolLM2; new fallback is multilingual).
- bundled GRPO baseline reward (forgelm/grpo_rewards.py +
  _math_reward_fn) replaced the old 'no reward → ValueError' wiring.
- Test/CI surface grew with test_quickstart_hardening, test_grpo_math_
  reward, test_grpo_format_reward, test_wizard_byod, test_cli_quickstart_
  wiring, test_packaging, plus a wheel-install-smoke nightly job.

Also corrected example counts in the release notes and CHANGELOG to
match the actual JSONL line counts (58/59/49/40 — same numbers used in
LICENSES.md), and added a per-template fallback table to the quickstart
guide so the auto-downsize behavior is no longer described with a stale
single-example sentence.

releases.md status banner softened from 'Released' to 'Merged to main
via PR #9; tag + PyPI publish pending' so it matches roadmap.md and
phase-12-quickstart.md, since 'git tag -l v0.4.5' is empty.
@sourcery-ai sourcery-ai Bot mentioned this pull request Apr 26, 2026
cemililik added a commit that referenced this pull request May 6, 2026
…gent review findings

- Wave 5 PR #34 round 3 absorption: full sweep of CodeRabbit's 6 OOD +
  20 Minor inline review comments PLUS 24 multi-agent review findings
  (code-correctness / privacy-security / standards / test-rigor docs
  under docs/analysis/code_reviews/wave5-*.md).

HIGH/Major absorptions:
- 4 broken `forgelm.cli.subcommands.*` symbol citations across
  purge/approvals reference docs (en+tr): _run_row_erasure →
  _run_purge_row_id, _handle_pending → _run_approvals_list_pending,
  _handle_show → _run_approvals_show (F-W5-PRIV-02).
- configuration.md (en+tr): drop `kserve | triton` ghost choices and
  the entire `## deployment:` ghost YAML block (no `DeploymentConfig`
  in ForgeConfig; extra="forbid" rejects it at load) — replace with a
  v0.6.0+ Phase 14 callout (F-W5-S1, F-W5-S2).
- ingestion.md (en+tr): drop conflicting "instructions format
  default" claim; document raw-chunk JSONL with summary-only
  --output-format {text,json} (CR-OOD ingestion).
- closure-plan PR #33 status: line 131 said "PR #33 pending" but
  line 119 said merged with `01e40ba` (CR-OOD closure-plan).
- leakage.md (en+tr): remove `audit.leakage_check.remove_cross_split_overlap`
  knob reference — knob doesn't exist in code; replace with the
  documented jq-based removal flow (CR-OOD leakage).
- TR + EN experiment-tracking: drop `$ ` prompt prefix in `text`-tagged
  preview blocks for `forgelm compare-runs` (CR-OOD experiment-tracking).
- roadmap mermaid (en+tr): add Phase 12.6 (Closure Cycle) → v0.5.5
  node + edge so visual flow matches table (CR-OOD roadmap).

Minor absorptions:
- phase-12-6-closure-cycle.md L123: stale "(currently 0.5.1rc1)"
  parenthetical → already-bumped during Wave 5 (F-W5-S4 / CR Minor).
- gdpr-erasure.md (en+tr): rename duplicate `### What gets recorded`
  / `### Neyi kaydeder` H3 to disambiguate Article 17 vs 15 sections
  (CR Minor #2 + #20).
- verify-gguf.md (en+tr) L147: revise misleading "still match SHA-256"
  hash explanation; clarify magic-header layer doesn't catch
  truncation independent of sidecar (CR Minor #3).
- verify_audit link mismatch in 4 reference docs (en+tr × verify_gguf
  + verify_annex_iv): label text now matches href (CR Minor #4 + #8).
- air_gap_deployment-tr.md L19: add `text` language tag to ASCII
  diagram fence (CR Minor #6).
- site/index.html L374: drop internal "Phase 19" milestone leak; tag
  reads `embed` (user-facing label) (CR Minor #7).
- air_gap_deployment.md L193: fix dangling sentence fragment in
  HF_TOKEN gated-models tip (CR Minor #9).
- performance.md (en+tr) L184: drop ghost `forgelm audit
  --output-format parquet`; replace with pandas/pyarrow conversion
  step (CR Minor #10).
- phase-11-5-backlog.md L10-12: link text → package paths matching
  href (forgelm/data_audit.py → forgelm/data_audit/, etc.) (CR Minor #11).
- testing.md L140: stale `23/23` → `39/39` bilingual parity count
  (CR Minor #12).
- testing.md L198: `python3` prefix consistency in checklist
  (CR Minor #13).
- supply-chain.md (en+tr): bandit cadence aligned to standards doc
  (Nightly 03:00 UTC + release tag, not "every PR + main push") (CR
  Minor #14 + #15).
- translations.js L1927: "Three values" → "Two values" (only
  forgelm-lang + forgelm-theme listed) (CR Minor #16).
- approvals_subcommand.md (en+tr) L26: staging path → match L84 JSON
  sample (`final_model.staging.<run_id>` canonical, run-id-less form
  honoured as fallback) (CR Minor #17 + #19).
- translations.js L1445/L1462/L1504/L1521 (en+tr): `<code>reject</code>`
  → `<code>forgelm reject</code>` for fully-qualified copy/paste
  parity (CR Minor #18).
- _meta.yaml header: add formal DE/FR/ES/ZH deferred-policy banner
  per closure-plan Task L (F-W5-S6).
- release.md L20 + L359: stale "0.3.1rc1 current" + "no
  __version__" claims; corrected to 0.5.5 + reflect that __version__
  + __api_version__ ARE exported per Library API standard (F-W5-S3).
- purge_subcommand.md (en+tr): add WARNING-on-fallback language for
  --output-dir asymmetry vs reverse-pii (F-W5-PRIV-05).
- verify-audit.md (en+tr): replace overstated "escrow" wording with
  KMS/HSM-based separation (F-W5-PRIV-06).

Tool tightening + new tests:
- check_cli_help_consistency.py: tighten _FORWARD_REF_MARKERS to
  whole-phrase forms (drop bare "planned"/"future"/"roadmap" which
  matched "planned attacks" / "future work" / "roadmap-driven" in
  unrelated prose); add Turkish equivalents for bilingual docs
  (F-W5-01).
- test_check_cli_help_consistency.py: 3 new regression tests:
  bare-substring negative (F-W5-01), >±3-line forward-ref window
  negative (F-W5-TR-01a), and Wrong:-block scope-to-one-block
  resumption (F-W5-TR-01b). Suite 17 → 20 tests, all green.

User-manual orphan fix:
- _meta.yaml: register `reference/json-output` page (existed in
  filesystem since Wave 2 but never declared in nav config — was
  invisible on the live site). Now declared = on-disk = 65 each.

Skipped (with reason):
- CR Minor #5 (library_api.md `forgelm.__version__` example):
  INVALID — verified `python3 -c "import forgelm; print(forgelm.__version__)"`
  prints 0.5.5 cleanly; both __version__ and __api_version__ ARE
  exported per Library API standard. CodeRabbit's web-search must
  have hit pre-Wave-5 PyPI. Doc is correct; code matches.
- F-W5-S5 (parity tool autodiscover usermanuals): registry shape
  refactor (~50 new pairs) is scope creep for round 3; defer to v0.6.
- F-W5-S8 (compliance_summary-tr.md): explicitly deferred to v0.6 by
  the finding itself.
- F-W5-S9, F-W5-TR-11/12 (NIT cite/style): non-load-bearing.
- F-W5-02/03/04 (cognitive complexity, unterminated fence,
  resume-after-Wrong unit test): refactor scope; covered by F-W5-TR-01
  block-scope tests for the resume case.
- F-W5-TR-02 (cleanup commit baseline regression test): the live CI
  --strict gate IS the guard; doubling up the integration-test
  layer is defensible but not blocking.
- F-W5-TR-03/05/06/07/08/09/10 (extras): non-blocking coverage gaps.

Self-review gauntlet (all green):
  ruff format/check ✅
  bilingual_parity --strict 39/39 ✅
  anchor_resolution --strict 264/264 ✅
  cli_help_consistency --strict 425/425 ✅ (reduced from 449 due to
    intentional invocation removals: ghost YAML block, parquet ghost,
    4 broken symbol-cite paths, etc.)
  site_claims --strict (artefacts/templates/GPU/version) ✅
  field_descriptions --strict ✅
  pytest 1433 passed / 14 skipped (+5 new tests vs round 2) ✅
  forgelm --config config_template.yaml --dry-run ✅
  build_usermanuals.py: en/tr 65/65 OK, JS bundles regenerate ✅

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant