Skip to content

Full-project-review remediation: Waves 1–4 (H2→N1, 78 commits)#63

Merged
cemililik merged 106 commits into
mainfrom
development
Jun 15, 2026
Merged

Full-project-review remediation: Waves 1–4 (H2→N1, 78 commits)#63
cemililik merged 106 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Full-project-review remediation — Waves 1–4 (H2 → N1)

Closes out the 2026-06-10 full-project review action plan. Wave 0 (7 CRITICAL release-blockers) and Wave 1/H1 (trainer/model dispatch) already merged in #61; this PR implements every remaining work-package — 78 commits, each a focused fix shipped with its regression test in the same commit.

Coverage (22/22 work-packages complete)

Wave Packages Highlights
W1 HIGH H2–H11 safety-gate threshold validators + fail-closed degenerate probes + causal-LM-as-classifier refusal (H2); GRPO math-reward end-anchoring (H3); approve→resume + terminal-state reset (H4); compliance HMAC fail-open / atomic Annex-IV exports / config_hash binding (H5); config schema bounds + deprecation cadence (H6); exit-code & JSON-envelope doc contracts (H7); unified URL masking + path-traversal-safe erasure (H8); non-string training-cell rejection (H9); CLI↔docs drift + wizard cancel/back/non-tty (H10); guard wiring + test_model/test_data + TR-link guard (H11)
W2 MED M1–M7 trainer/OOM/gate robustness, compliance completeness, config bounds, data-audit/ingestion, CLI exit-code/JSON gaps, TLS-port/purge-redaction/integrity-symlink security, doc/guard wiring
W3 LOW L1–L4 trainer/eval/reward/judge/merging cleanup, compliance/security LOW, config/CLI LOW, data/docs LOW + remaining guard own-tests
W4 NIT N1 docstring/comment/§-ref/SBOM-ordering hygiene sweep

Method

Implemented via 3 sequential multi-agent workflows on a shared tree (no merge conflicts). Each agent verified every finding still reproduced at HEAD before acting — fixing it with a regression test, or skipping it with a one-line reason when prior adjacent work had already resolved it (~30 such already-fixed/invalid/duplicate skips, all documented in the run logs). House policy honoured: behaviour changes ship with tests; bilingual EN+TR doc mirrors kept in lockstep; the append-only audit-event catalog kept in sync.

Validation (green at HEAD)

  • pytest tests/: 2718 passed, 17 skipped, 0 failed (coverage 80%).
  • ruff format --check . + ruff check .: clean (237 files).
  • forgelm --config config_template.yaml --dry-run: exit 0.
  • All tools/check_*.py guards green — including the newly-wired check_doc_numerical_claims, check_notebook_pins, check_tr_links_prefer_mirror, check_bilingual_code_blocks, check_yaml_snippets, check_module_size, check_site_chrome_parity; the unwired-guard allowlist is now empty (every guard is CI-wired).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added forgelm verify-integrity subcommand to validate model artifacts against training-time SHA-256 manifest
    • Configuration-driven TIES/DARE merge hyperparameters and synthetic data success-rate thresholds
    • Early stopping patience and sequence packing controls in training configuration
    • Webhook require_https enforcement and pipeline manifest integrity checking
  • Changed

    • Renamed training.sample_packing to training.packing (deprecated alias with v0.9.0 removal)
    • Stricter config validation: distributed strategy now requires deepspeed or fsdp; data mix ratios enforce cardinality matching
    • Safety evaluation treats max_safety_regression as absolute unsafe-ratio ceiling (not baseline-relative)
    • Updated deprecation targets to v0.8.0 for --data-audit flag and retention.staging_ttl_days
  • Fixed

    • Config validation fail-fast on invalid inputs; OOM recovery rebuilds with user callbacks; audit integrity hardened against re-rooting

cemililik and others added 30 commits June 14, 2026 17:30
…egenerate probes; refuse causal-LM-as-classifier (H2 / XP-06, F-P1-FAB-04,07,08, F-P3-FABLE-05,15,16,17)

Config (XP-06): reachable SafetyConfig/JudgeConfig/BenchmarkConfig states
silently disabled a configured auto-revert gate while the run still
reported passed=True. Add ge/le bounds to the five float threshold
fields (safety 0-1, judge 1-10, benchmark 0-1) and a SafetyConfig
model_validator that rejects min_safety_score under non-confidence_weighted
scoring, restricts severity_thresholds to the known severity vocabulary
with 0-1 values, and auto-enables track_categories when severity_thresholds
is set so the gate is actually enforced. Export SEVERITY_LEVELS as the
single source of truth shared with the runtime severity_dist counters.

Runtime fail-open closures:
- F-P3-FABLE-05/16: an existing-but-empty / all-blank / wrong-schema
  probes file now fails CLOSED (passed=False, evaluation_completed=False),
  symmetric with the missing-file path, instead of returning a vacuous
  passed=True. _load_safety_prompts skips and counts rows that yield no
  usable prompt instead of feeding empty-string probes.
- F-P3-FABLE-17: refuse a causal-LM checkpoint (e.g. the default
  Llama-Guard-3-8B) loaded via pipeline('text-classification') when its
  classification head is newly-initialized/absent (causal-LM architecture
  + placeholder LABEL_0/LABEL_1 labels) — every verdict would be garbage.
  Generation-based Llama-Guard scoring is deferred; the minimal
  refuse-with-actionable-error fix the finding offers is implemented.

Adds an evaluation_completed flag to SafetyResult so callers can tell
"could not evaluate" from "evaluated and failed the gate".

Regression: tests/test_config.py::TestSafetyGateValidation,
tests/test_safety_advanced.py::TestDegenerateProbeFailClosed,
::TestClassifierHeadValidation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, enable track_categories, record total_count (H2 / F-P3-FABLE-12,13,16)

Standalone `forgelm safety-eval` (F-P3-FABLE-12/13):
- A classifier that never loaded (or a probes file with no usable
  prompts) is a runtime/config problem, not a threshold failure. The
  dispatcher now exits EXIT_TRAINING_ERROR (2) when run_safety_evaluation
  flags the run with evaluation_completed=False, matching the documented
  exit-code contract (was exit 3).
- Construct and pass an AuditLogger so the documented Article-15
  audit.classifier_load_failed event can actually fire on this surface
  (its emission guard previously always skipped).
- Pass SafetyEvalThresholds(track_categories=True) so the advertised
  per-category breakdown is reachable instead of always {}.

Training gate (F-P3-FABLE-16): the safety.evaluation_completed audit
payload now carries total_count so a vacuous pass (zero probes
evaluated) is distinguishable from a real 100%-safe evaluation in the
append-only trail. Catalog row updated (EN + TR).

Regression: test_verification_toolbelt.py::TestSafetyEvalDispatcher
(classifier-load → exit 2; track_categories + audit_logger wired);
test_human_approval_gate.py::...::test_safety_audit_event_records_total_count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…F-P3-FABLE-14, F-P7-OPUS-15)

The safety-eval dispatcher explicitly refuses .gguf paths with a config
error, but four user-facing surfaces still claimed GGUF support and the
GGUF-export manual instructed operators to run a safety re-check against
the quantised artefact (a command that exits 1). Align all surfaces:

- the safety-eval subparser description and --model help no longer
  mention GGUF;
- the top-level CLI epilog says "HF checkpoint";
- the EN + TR GGUF-export and deploy-targets manuals now tell operators
  to run safety-eval against the pre-export HuggingFace checkpoint and
  treat post-quant safety drift as a known limitation.

No new behaviour; CLI ↔ docs help-text consistency restored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…H3 / F-P2-FAB-06, F-P3-FABLE-27, F-P2-FAB-42, F-P3-FABLE-63)

_math_reward_fn graded the FIRST "Answer:" occurrence via leftmost .search
while the format reward is end-anchored, so a self-correcting completion
("Answer: 5 … Answer: 7") earned format reward on its final answer but
correctness reward on an earlier discarded candidate — a reward-hackable
divergence. Grade the LAST marker (finditer → last) to match the format gate.

Unify the two divergent module-private _ANSWER_PATTERN constants: the
capturing extraction regex now lives once in grpo_rewards as the shared,
documented ANSWER_EXTRACT_PATTERN (imported by trainer); the format gate's
end-anchored pattern is renamed _ANSWER_END_PATTERN with its rstrip contract
documented, removing the same-name/different-contract collision.

Make combined_format_length_reward's zip strict=True, consistent with the
fail-loud strict zip in _math_reward_fn.

Regression tests: last-vs-first anchoring (correct-final + reward-hack→0),
trailing-prose last-marker grading, cross-module correctness/format agreement,
and combined-reward strict-zip-raises-on-mismatch. Bilingual alignment guide
updated (EN + TR) to state final-marker grading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e (H4 / F-P2-FAB-03, F-P2-FAB-04)

F-P2-FAB-03: after `forgelm approve` renames the gated staging dir to
final_model/, `--resume-from` now recognises the stage as approved
(staging gone + promoted path present), skips it, rewrites its
output_model to the promoted path, and chains downstream from the
approved weights — instead of re-training the gated stage. An unapproved
gate (staging still on disk) still refuses per C3.

F-P2-FAB-04: a `--resume-from` run now resets the prior run's terminal
fields (final_status→in_progress, stopped_at/finished_at→None) so a
resumed-successful run no longer carries a stale stopped_at_stage /
gated_pending_approval status; `_finalise_pipeline` then rewrites and
emits exactly one `pipeline.completed` with the corrected status.

Regression tests cover: post-approval resume skips the gated stage and
chains from final_model/ with one stage_started per stage_index;
resume-to-success after a failure resets fields + emits corrected
completion; resume-to-success after a gate finalises as completed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-OPUS-01, F-P6-OPUS-15)

clean_string silently coerced non-string payloads (dict/list/int via str(),
None/falsy -> "") into training data while the modern messages path
(_process_messages_format) raises loudly on the same shapes. This baked
Python repr strings and empty responses into the corpus with exit 0 and no
error -- silent-wrong-training. Make clean_string raise ValueError on any
non-str cell, symmetric with the messages path; wrap the text and
User/Assistant formatters to add row-index context to the raised error.

Policy: raise (not skip-with-count) -- matches the in-repo messages-path
contract and the batched .map() has no per-row drop mechanism. Callers
already omit genuinely-absent optional cells (missing system prompt) before
reaching clean_string, so legitimate empties still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, atomic exports (H5 / F-P4-OPUS-03,04,05,06,10,11,13,15,33)

- verify_audit_log refuses require_hmac=True with hmac_secret=None at the
  library boundary instead of returning valid=True after a presence-only
  check (fail-open closed).
- verify-audit / verify-annex-iv exit-code docs (parser help, module
  docstring, usermanual EN+TR) reconciled to the shipped exit 1 contract.
- Single-stage training manifest, human_approval.required event, and the
  training JSON envelope now carry config_hash (+run_id) via a shared
  compute_config_hash helper, mirroring the pipeline's config-binding.
- export_compliance_artifacts writes the 5-artifact Annex IV bundle
  all-or-nothing (staging dir + os.replace promotion); export_evidence_bundle
  writes tmp+rename; the trainer emits compliance.artifacts_export_failed on
  export failure and fires compliance.artifacts_exported even when the
  secondary Article-10 governance report fails (carrying governance_ok).
- New audit event compliance.artifacts_export_failed catalogued EN+TR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…romote_strategy drift (H5 / F-P4-OPUS-02,07,08,32; F-P5-OPUS-03; F-P7-OPUS-17)

- All six EN+TR doc sites claiming the Windows AuditLogger uses
  msvcrt.locking now state the truth: no cross-process lock on Windows
  (advisory flock helper is a no-op); use a distinct output_dir per run.
  Code comment tightened from 'advisory-only' to 'no lock acquired'.
- Webhook vocabulary corrected from 'five' to 'eight' everywhere
  (standard, audit_event_catalog EN+TR, webhooks usermanual EN+TR): the
  three pipeline.* events are now documented alongside the five lifecycle
  events, with the payload-schema enum and the canonical_webhook_events
  helper docstring updated to match the code's actual 8 events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rip (H6 / F-P1-FAB-03,05,06)

- DistributedConfig.strategy: Optional[str] -> Optional[Literal["deepspeed","fsdp"]]
  so unsupported strategies (horovod, ddp, "DeepSpeed") fail at config time
  instead of silently running single-GPU at trainer.py.
- DataConfig: reject non-finite mix_ratio weights (NaN/inf) and add a
  cross-field model_validator requiring len(mix_ratio) == 1 + len(extra_datasets);
  data._merge_extra_datasets now raises loudly on a length mismatch instead of
  silently falling back to uniform mixing.
- merge_pipeline_stage_config: dump with exclude_unset=True so unset defaults
  (evaluation.staging_ttl_days=7) no longer round-trip as explicitly-set and
  falsely raise ConfigError for pipeline + retention + evaluation configs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ document include_eval_samples (H6 / F-P1-FAB-15,16,09)

F-P1-FAB-15 (sample_packing): the documented-but-unconsumed field was a
silent no-op. It now forwards to `packing` (TRL's single packing knob) at
config time with a DeprecationWarning + logger.warning, so following the
docs actually fires. Reconciled the contradictory "requires"/"mutually
exclusive" descriptions across configuration.md (+TR), usermanuals/sft.md
(+TR), troubleshooting.md (+TR), usage.md (+TR), config_template.yaml,
roadmap/releases.md, design/blackwell_optimized.md — all now point at
`packing`; removal scheduled v0.9.0.

F-P1-FAB-16 / XP-16 (deprecation cadence): v0.7.0 shipped without the
promised removals of evaluation.staging_ttl_days and --data-audit. Deferred
both to v0.8.0 (the deferral was already recorded in _dispatch.py) and
retargeted every stale "v0.7.0" stamp consistently: config.py warnings /
ConfigError / docstrings, _parser.py --data-audit help, configuration.md
(+TR) deprecation notes, usermanuals/cli.md (+TR), qms SOP (+TR),
release.md worked example. Added an [Unreleased] CHANGELOG note recording
the deferral; the ### Removed section lands in v0.8.0.

F-P1-FAB-09 (include_eval_samples): documented the v0.7.0 privacy-by-default
opt-in fields (evaluation.safety.include_eval_samples and
evaluation.llm_judge.include_eval_samples) in configuration.md + TR mirror,
plus a retroactive [0.7.0] ### Changed CHANGELOG note describing the
artifact-content change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The training-mode `--dry-run --output-format json` envelope keyed only on
`status: "valid"` with no `success` key, breaking the universal envelope
contract (json-output.md "Common conventions": every envelope starts with
`success`). A CI consumer reading `result["success"]` got a KeyError on the
happy path — the most common CI pre-flight invocation.

- Prepend `success: True` as the first key in `_build_dry_run_result`
  (keep `status: "valid"` for backward compat); skip both wrapper keys in
  the text-mode summary loop.
- Document the dry-run config-validation envelope in json-output.md (+ TR
  mirror) with the full key set and exit-code mapping.
- Regression test: test_dry_run_json_envelope_has_success_true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (H7 / F-P1-FAB-14)

error-handling.md exit-code table row 0 claimed exit 0 means "all gates
passed", but with the shipped default `evaluation.auto_revert: false` a
failed benchmark/safety/judge gate is recorded (JSON gate block,
`*_passed: false`) yet the model is still promoted and the run exits 0.
The same false guarantee appeared across the user-facing surface.

- error-handling.md row 0: qualify the exit-0 guarantee by `auto_revert`.
- exit-codes.md (EN+TR): fix row 0, row 3 (was implying exit 3 fires
  without a revert), and the "What exit 0 actually guarantees" section.
- judge.md (EN+TR): fix the inverted "otherwise exits non-zero" claim.
- logging-observability.md + audit_event_catalog.md (EN+TR): reword the
  `training.success` / `pipeline.completed` "All gates passed" descriptions
  (the event also fires on the no-revert gate-failure path).
- Regression tests pinning the load-bearing behaviour: a failed benchmark
  gate continues (returns True, records benchmark_passed=False) under
  auto_revert=false, and reverts+halts (returns False) under auto_revert=true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…real one (H7 / F-P1-FAB-11)

`tools/regenerate_config_doc.py` was cited as the Phase 16 config-drift
control in the QMS change-management SOP (EN+TR §4.2), the ISO 27001 /
SOC 2 design doc, check_field_descriptions.py's docstring, and a ci.yml
comment — but the file never existed in git history and no config-doc
diff-guard was ever wired. An auditor following the SOP would find a
documented control that cannot be executed or evidenced.

Option (b) from the finding (smaller honest diff): rewrite all four
production-tree sites to describe the real control —
`check_field_descriptions.py --strict` (CI) + manual doc review +
`check_bilingual_parity.py --strict` — and record the correction in the
append-only SOP review log.

- Regression test (test_phantom_tool_citations.py): no tracked doc/code
  presents `regenerate_config_doc` as a live control; the real control
  file exists; the scanner docstring no longer cites the phantom companion.

Note (out of H7 scope): other public-tree files still cite tools that do
not exist (docs/design/library_api.md → check_api_compat.py /
check_api_doc_completeness.py; docs/roadmap/* → check_webhook_event_vocabulary.py,
check_test_naming.py, manual_smoke.py et al). A blanket "every cited
tools/*.py must exist" guard belongs with the guard-apparatus work (H11);
this test is deliberately targeted at the F-P1-FAB-11 phantom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ort-error logs (H8 / F-P5-OPUS-01,06,11)

safe_post/safe_get logged the raw requests/urllib3 exception string, which
embeds the request URL path — webhook bearer tokens (Slack/Teams/Discord/
custom carry the secret in the path) leaked into the forgelm._http WARNING
log on any transport failure. Add _redact_url_paths_in_text: an exact
known-URL pass plus a bounded, ReDoS-safe 'url: <path>' token pass, applied
after header masking in both helpers.

webhook._mask was a second URL masker that appended the first path segment,
leaking the token for custom receivers shaped https://host/<TOKEN>. Collapse
it to a thin wrapper over the single _http._mask_netloc chokepoint so the
masking policy lives in one place (no update-one-not-the-other drift).

Regression tests: transport-error path-redaction for safe_post + safe_get +
HEAD; _mask never leaks the secret across receiver shapes and equals
_mask_netloc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ommonpath (H8 / F-P5-OPUS-02)

forgelm purge --run-id <id> --kind staging took --run-id verbatim (free-form
type=str, no charset constraint) into final_model.staging.<run_id>, gated
only by Path.exists() before shutil.rmtree. A '..'-bearing run_id could
resolve outside output_dir → arbitrary-directory delete — the exact
commonpath defence-in-depth the design (gdpr_erasure.md §4.4) claims but
never had.

Add _path_inside_output_dir (mirrors _approve._staging_path_inside_output_dir:
realpath + commonpath) and reject any resolved target that escapes output_dir
before the dry-run report or deletion. Fail-closed (whole batch refused),
EXIT_CONFIG_ERROR, with a data.erasure_failed event (error_class=
PathTraversalRefused) — no new dotted audit event, so the catalog is unchanged.

Regression tests: traversal run_id refused (victim untouched, exit 1,
request→failed not completed); _path_inside_output_dir accept/reject unit
coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… / F-P5-OPUS-04)

supply_chain_security.md (EN + TR) still described the pre-2026-05-24
UNKNOWN policy ('::warning::, nightly stays green' / 'yeşil kalır') after
check_pip_audit.py flipped to fail-closed (exit 1, ::error:: per finding).
pip-audit's JSON omits OSV severity so UNKNOWN is the dominant path — the
doc told deployers their gate was advisory when it actually fails the run.
Rewrite the UNKNOWN bullet in both languages to the fail-closed policy +
the opt-in ignore-file triage path.

Regression test: a doc-consistency guard asserting the UNKNOWN bullet says
'exit 1' and no longer declares 'stays green' / 'yeşil kalır' in either
language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…H10 / F-P7-OPUS-05,06,07,08)

- _check_navigation_token now raises WizardCancel on cancel/q/c/quit so the
  welcome banner's 'type cancel to exit cleanly' promise holds at every step
  (F-P7-OPUS-05). The step machine and the quickstart/BYOD prelude convert it
  to a clean WizardOutcome(None) -> EXIT_WIZARD_CANCELLED; the BYOD prelude
  still treats cancel as 'fall back to the full wizard'.
- run_wizard_full wraps the prelude so back/reset raised before the step
  machine no longer escape as an uncaught traceback (exit 1); they fall
  through to the full wizard instead (F-P7-OPUS-08).
- _maybe_run_wizard refuses '--wizard --output-format json' with a proper
  {success:false} envelope on stdout + exit 5, and routes the
  --wizard-start-from guidance warning to stderr in JSON mode so it never
  precedes the dispatcher envelope (F-P7-OPUS-06/07).
- Regression tests for cancel-in-step-machine, cancel-in-BYOD-fallback,
  back/reset-in-prelude, JSON-mode refusal, and stderr/stdout warning routing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0 / F-P7-OPUS-01,02,03,09,10,11,12,13,14,18)

One CLI-docs sweep (EN+TR mirrors kept in spine sync):
- Legacy --data-audit alias now passes enable_quality_filter=True so it
  matches the 'identical behaviour' claim vs the audit subcommand (-01).
- cli.md exit-codes table: drop the phantom 130 row (Ctrl+C is clamped to 2)
  and disclose that argparse usage errors exit 2 while post-parse config
  validation exits 1 (-02/-03).
- json-output.md checks[].status enum corrected to pass/warn/fail; a crashed
  probe surfaces as fail + extras.crashed (-09).
- cache-tasks --output help advertises the Datasets cache chain it actually
  uses, not the Hub chain (-10).
- cli.md doctor summary drops the non-existent audit-secret probe claim (-11).
- json-output.md gains forgelm export + forgelm deploy envelope sections (-12).
- cli.md stops advertising comma-separating the single-value --quant choices
  flag (-13); check_cli_help_consistency now flags comma-list prose claims on
  any choices flag.
- chat --help + chat.py docstrings drop the unshipped per-response safety
  annotations claim (-14).
- cli.md describes reject as PRESERVING staging for forensics, not
  'discard staging' (-18).

Regression tests: legacy-alias quality filter, chat-help no-safety, export
comma-quant rejection, export/deploy envelope doc parity, cache-tasks help,
doctor status-vocabulary + probe-list doc parity, exit-code/reject doc
consistency, and four new prose-claim guard tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dules (H11 / F-P8-C-02, F-P8-C-03)

model.py loading core (_resolve_bnb_compute_dtype ValueError, _device_map_for,
_build_model_kwargs quantization gating, _load_tokenizer pad-token,
_build_lora_config / _resolve_peft_flags) and data.py format-processing +
column-validation raises (clean_string, _process_messages_format,
_process_user_assistant_format, _validate_trainer_columns, _apply_mix_ratio,
_merge_extra_datasets) previously ran only via lambda stubs or export-symbol
assertions. Both now have direct no-GPU/no-network behavioural coverage of
their raise paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (H11 / F-P8-C-08)

The guard was wired into ci.yml but untested, and bypassable: the third pattern
matched only ``sys.modules["torch"] = None`` while ``= object()`` /
``= SimpleNamespace()`` / ``= fake`` corrupt the session identically, and the
per-physical-line scan missed the multi-line ``pop(\n "torch")`` split.

- Broaden the assignment pattern to ``= <anything>`` (negative lookahead on
  ``==`` so equality comparisons stay clean).
- Rejoin logical lines before matching so multi-line pop/del forms are caught.
- Add tests/test_check_no_unguarded_sys_modules_pop.py covering each detected
  form, each sanctioned form, the live-repo clean pass, and CI wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… TR cross-link leaks (H11 / XP-13, F-P8-C-04, F-P8-C-07, F-P8-C-13)

Guard apparatus had dead arms: of 19 tools/check_*.py guards, ten ran in no
workflow and three gauntlet-listed guards ran only by human discipline.

Wiring (ci.yml validate job):
- check_library_api_doc, check_bilingual_code_blocks, check_yaml_snippets,
  check_site_chrome_parity (default mode — --strict gates the deferred i18n
  backlog), check_module_size, and the three gauntlet-only guards
  (check_wizard_defaults_sync, check_no_analysis_refs, update_site_version).
- New check_tr_links_prefer_mirror guard (F-P8-C-04) wired into ci.yml AND the
  CLAUDE.md/AGENTS.md gauntlet.

Self-enforcement:
- tests/test_guard_wiring.py meta-test: every tools/check_*.py is wired OR
  allowlisted with a rationale; every gauntlet guard is in a workflow; the
  CLAUDE.md/AGENTS.md gauntlets stay in lockstep. Only check_doc_numerical_claims
  (W1/H5) and check_notebook_pins (W2/M7) remain allowlisted with owner notes.
- Own-tests for the previously-untested guards: bilingual_code_blocks,
  library_api_doc, yaml_snippets, site_chrome_parity.

TR cross-link sweep (F-P8-C-04):
- 50 of 62 in-prose cross-links across 19 *-tr.md pages routed Turkish readers
  to the English sibling despite an existing -tr.md mirror; rewritten to the TR
  mirror (translated link text preserved). The **Ayna:** backlinks are exempt.
- localization.md "Structural mirror rule" clarified to make the rule explicit.

Co-located fixes folded in (house policy):
- yaml-templates.md (EN+TR): severity_thresholds used invalid Llama-Guard
  category codes (S1/S5/S10) instead of severity levels — failed
  check_yaml_snippets; fixed to critical/high/medium/low.
- check_no_analysis_refs: allowlist tests/test_phantom_tool_citations.py
  (functional path filter, not a content citation) so the now-wired guard is
  green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eps (M1 / P3-FABLE-22,23,24,25,26 + P2-FAB-09,15,20)

- OOM rebuild now reuses the user-supplied callbacks captured in train()
  instead of the live callback_handler list (which already carries HF's
  instantiated defaults), so DefaultFlow/Progress callbacks are no longer
  duplicated and EarlyStopping patience no longer leaks across retries
  (F-P3-FABLE-22).
- oom_recovery_min_batch_size gains a ge=1 Field bound + a defensive
  max(min_bs, 1) clamp in the loop, so a 0/negative floor can no longer
  drive new_bs to 0 → ZeroDivisionError; the clean "cannot recover"
  diagnostic fires instead (F-P3-FABLE-23).
- execute_evaluation_checks decouples detection from reversion: a
  configured eval-loss threshold/baseline (and NaN/Inf divergence) is now
  always evaluated and logged even when auto_revert=false — previously the
  whole check was skipped, letting a diverged model ship with exit 0 and no
  signal (F-P3-FABLE-24).
- Benchmark gate fails fast at the config preflight when lm-eval is missing
  (install hint, before training), and the three gate-runner import guards
  re-raise ImportError-with-hint instead of swallowing to None, so a
  configured gate can never silently degrade to a skip with exit 0
  (F-P3-FABLE-25).
- Safety infrastructure-failure results set safe_ratio=0.0 explicitly so
  the trainer-side metric / audit payload no longer report a perfect 1.0
  safe ratio for an evaluation that classified zero responses
  (F-P3-FABLE-26).
- train() moves _build_trainer inside the try so a construction failure
  still emits pipeline.failed + notify_failure after notify_start fired
  (F-P2-FAB-09); regression tests cover the OOM loop, gate import contract,
  and train() failure path (F-P2-FAB-15, F-P2-FAB-20).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…P2-FAB-07,11,12,13)

- Re-executing a stage now clears every attempt-scoped field (error,
  skipped_reason, gate_decision, auto_revert_triggered, exit_code,
  finished_at, duration_seconds, output_model) loaded off the resumed
  state file, so a resumed-to-success stage no longer persists
  status="completed" next to a stale error="Trainer crashed: …"
  (F-P2-FAB-07).
- `--stage <name>` no longer falsely refuses a stage that declares its
  own `model:` block: such a stage ignores the auto-chain seed, so the
  prev-output-on-disk demand is a spurious refusal (F-P2-FAB-11).
- A `--stage` run on a pipeline that already ran preserves the prior
  run's completed stage history (status / output_model / metrics) via
  _init_state_preserving instead of clobbering pipeline_state.json — a
  later `--resume-from` then correctly skips stages whose final_model/
  is on disk rather than re-training them (F-P2-FAB-12).
- run_pipeline_from_args gains a top-level exception boundary mirroring
  cli/_training.py: a mid-run _save_state OSError maps to
  EXIT_TRAINING_ERROR (2) with the 2-key JSON envelope on stdout instead
  of a raw traceback + interpreter exit 1 colliding with
  EXIT_CONFIG_ERROR; the stage-filter predecessor merge is now guarded
  to route config failures to exit 1 cleanly (F-P2-FAB-13).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ash routing (M1 / P2-FAB-16,17,18)

- TestGateApplication exercises the safety/judge gate revert vs continue
  matrix (auto_revert on/off), the staging_path-clearing on revert, the
  None-gate no-op path, and the F-P3-FABLE-26 trainer-side assertion that
  an infra-failure SafetyResult yields safe_ratio=0.0 in both the metric
  and audit payload (F-P2-FAB-16).
- TestBaselineLossCapture pins _measure_baseline_loss's gating condition,
  the disable_adapter happy path + fallback-on-error, the missing-eval_loss
  no-arm path, and the already-configured / no-auto_revert skips
  (F-P2-FAB-17).
- TestPerStageTrainingErrorRouting drives a stage whose trainer raises and
  asserts EXIT_TRAINING_ERROR (2), chain halt, downstream
  skipped_due_to_prior_revert, and the "Trainer crashed:" stage error
  (F-P2-FAB-18).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-12,14,16,17,18,20)

M2 (W2-MED) compliance completeness:

- F-P4-OPUS-14: ship `forgelm verify-integrity <model_dir>` — the
  consuming Article-15 verifier that re-checks a model directory against
  its `model_integrity.json` SHA-256 manifest, reporting changed /
  removed / added artifacts (the generate side already shipped; only
  the verification half was missing). New subcommand wired through the
  parser, dispatcher, package facade, and EN+TR reference docs.

- F-P4-OPUS-16: `compute_annex_iv_manifest_hash` now normalises the
  payload through a JSON round-trip before canonicalising, so the
  writer (in-memory dict) and verifier (disk round-trip) hash identical
  structures even for non-JSON-native content (integer dict keys, sets).
  Previously such inputs produced a false-tampering verdict.

- F-P4-OPUS-17: the Annex IV §1 completeness gate now requires the
  identity-critical sub-fields (provider_name, system_name,
  intended_purpose) to be non-empty; the writer skips the artifact
  (returns None) when they are all blank rather than emitting a §1 stub.

- F-P4-OPUS-18: the post-rename `human_approval.granted` audit write in
  `approve` is wrapped so an OSError surfaces as a clean
  EXIT_TRAINING_ERROR with an operator-actionable AUDIT-GAP message
  instead of leaving the model promoted behind an uncaught traceback.

- F-P4-OPUS-20: `generate_pipeline_manifest` stamps a
  `metadata.manifest_hash` over the manifest, and `_verify_manifest_payload`
  recomputes it so content tampering of stage fields (metrics,
  gate_decision, output_model) that keeps the chain self-consistent is
  now detected. Absence downgrades to structural-only verification.

- F-P4-OPUS-12: regression tests for the two write-time hardening
  guarantees that previously had none — multi-writer re-read-under-lock
  (no chain fork) and the stale-genesis-manifest write-time ERROR path.

Every behaviour change ships its regression test in this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ext (M4 / F-P6-OPUS-02,04,05)

_process_messages_format silently emitted an empty training string for an
empty messages list ([]) — the exact 'corpus of empty rows' failure its
own docstring claims the loud-raise rewrite prevents. Now raises ValueError
with the row index.

clean_text was documented as stripping 'control characters' but the
" ".join(text.split()) body only collapsed whitespace controls; NUL/BEL/ESC
and the rest of the C0/C1 Cc set passed verbatim into the tokeniser. clean_string
now deletes non-whitespace control characters before collapsing whitespace,
honouring the config field's documented contract.

Adds a TestProcessMessagesFormat class pinning the loud-raise contract
(happy path + empty-list + non-str role/content + non-iterable msg_list +
non-dict message) plus control-char strip/preserve cases — closing the
zero-regression-test gap on this deliberate behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d (M4 / F-P6-OPUS-03)

The function docstring advertised 'Alphabetic-character ratio < 0.45.' but the
code compares against _FRONTMATTER_ALPHA_RATIO_MAX (0.30, tightened in the
round-3-follow-up review). The docstring now references the constant by name so
the active threshold can't drift again; the echoing test-class docstring is
updated to 0.30. Adds a docstring-parity regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… / F-P6-OPUS-06)

The structural ReDoS validator for operator --strip-pattern regexes rejected
the single-group (a+)+ shape but accepted the redundantly-wrapped ((a+))+b /
(((a+)))+b variants, which backtrack exponentially (n=24 -> 3.5s). When an
inner group closed, the parent's last_atom_unbounded flag was reset to False,
so the enclosing + no longer saw an unbounded inner atom. _close_group_or_raise
now propagates the closed group's unbounded flag up (outer-quantifier OR
inner-unbounded), so an enclosing quantifier still trips the reject through
extra grouping parens. Inner bounded tails (((a+)b)+c) still correctly reset.

Adds parametrized rejection cases for the wrapped shapes and safe-pattern
cases pinning no false positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t JSON lines (M4 / F-P6-OPUS-07,08)

The audit text extractor recognised only the messages list and a bare prompt
column, so the synthetic generator's instruction ({instruction,output}) and
chatml ({User,Assistant}) shapes extracted to '' (counted null_or_empty, never
scanned for PII/secrets) and prompt_response silently dropped its response half
— the teacher text most likely to carry memorised PII. _extract_text_payload
now folds the (user-half, assistant-half) instruction-tuning pairs via the new
_INSTRUCTION_PAIRS constant, joining both halves before the single-column fast
path so the response/output half is always scanned.

The seed-prompt and file-response loaders parsed each line with json.loads then
called .get() but only caught JSONDecodeError; a valid-JSON-but-non-object line
([1,2,3], 42, ...) crashed the whole --generate-data run with an uncaught
AttributeError. _load_seed_prompts now treats a non-dict line as a plain-text
prompt; _load_file_responses counts it as malformed and skips it, honouring its
'loud about failures' docstring.

Adds round-trip audit tests asserting PII in the response half is detected for
all four synthetic shapes, and loader tests feeding non-object JSON lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…19,20,21,22,23,24,25)

- deploy: tag DeployResult.error_kind so a non-directory model_path /
  unsupported target (caller-input errors) route to EXIT_CONFIG_ERROR(1),
  not EXIT_TRAINING_ERROR(2), matching the public exit-code contract
  (F-P7-OPUS-22).
- quickstart --list: wrap the template array in the universal
  {success, templates, count} envelope so consumers can branch on the
  single success key like every sibling list command (F-P7-OPUS-24).
- quickstart JSON mode: suppress the auto-launched interactive chat REPL
  so human prose never interleaves with the JSON envelope on stdout;
  record chat_launched=false in the envelope (F-P7-OPUS-25).
- wizard _load_defaults: catch json.JSONDecodeError (a ValueError, NOT an
  OSError) so a present-but-corrupt _defaults.json degrades to the
  hardcoded fallbacks instead of crashing the wizard at import (F-P7-OPUS-21).
- json-output.md (EN+TR): document the argparse-seam caveat (usage errors
  exit non-zero on stderr with no JSON envelope), add the forgelm
  quickstart section (list / generation-success / training-failure
  shapes), and correct the deploy exit-code mapping (F-P7-OPUS-19,23).
- tests: new test_cli_training_seam.py drives a real TrainResult/exception
  through _run_training_pipeline asserting the 0/2/3/4 + ConfigError(1)
  mapping (F-P7-OPUS-20); regression tests for each behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cemililik and others added 11 commits June 15, 2026 11:09
…PR#63-review)

Two GRPO-pipeline bugs sharing test_grpo_math_reward.py:

* grpo_rewards.ANSWER_EXTRACT_PATTERN: the greedy body absorbed a later
  same-line 'Answer:' token ("Answer: 5 Answer: 7" captured "5 Answer: 7"),
  so the LAST finditer match still graded the discarded candidate — reopening
  the reward-hacking divergence the last-marker rule closes. Guard every body
  run with (?!answer\s*:) so it refuses to cross into a new marker; markers now
  split into separate matches and the final one is graded. Fixed-prefix
  lookahead keeps scanning linear (the rejected (?![\s\S]*answer:) alternative
  rescanned the tail per marker → O(n^2), ~3s at n=5000). Also drop a stray
  Turkish word from the pattern comment (English-only policy).

* trainer._dataset_has_gold_answers: the column_names fallback used
  next(iter(train)), consuming the first row of a self-iterating / one-shot
  dataset (iter(train) is train) and silently altering the training stream.
  Detect that shape and trust presence-by-name instead of advancing the
  iterator; fresh-iterator datasets (HF Dataset / list) keep probing as before.

Regression tests: same-line multi-answer grading, many-marker ReDoS linearity,
and a one-shot iterator that must survive the probe intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… SNI (PR#63-review)

_PortStrippingAdapter.send delegates straight to HTTPAdapter.send to bypass
the parent's port-bearing assert_hostname derivation — but that also skips the
parent's SNI setup, so urllib3 would SNI the IP literal / port-bearing host and
fail TLS verification on endpoints like https://host:8443. Set server_hostname
to the bare (port-stripped) host alongside assert_hostname, and clear both when
no Host header is present so a stale value can't leak onto a reused session.
Extends the port-strip test to assert server_hostname and adds a no-Host-header
cleanup test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PR#63-review)

A malformed --type custom regex interpolated the raw re.error into the error
envelope (a redirectable durable record). re.error.msg / str(exc) reproduce
pattern-derived fragments — e.g. an invalid group name "(?P<alice@example.com>x)"
yields "bad character in group name 'alice@example.com'" — leaking the subject's
identifier the subcommand exists to keep out of durable records (F-P5-OPUS-17).
Emit a generic message with only the numeric offending position. Regression
test covers the group-name leak shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed line (PR#63-review)

A bare quoted-string seed line ("prompt text") fell into the else branch and
appended the raw line — keeping the JSON quotes — instead of the parsed string.
Add an isinstance(data, str) branch that appends the parsed value, matching
safety._load_safety_prompts. Regression test pins that the JSON quotes are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3-review)

CodeRabbit flagged several user-facing contract contradictions introduced
or missed by the doc sweep. Resolved against the canonical mappings:

- configuration.md (EN+TR usermanual): the "top-level config has N blocks"
  list contradicted the `deployment:` section that says the key is rejected
  by `ForgeConfig` (extra="forbid"). Dropped `deployment:` from the list and
  corrected the count 13 -> 12.
- verify-audit.md (EN+TR): the strict-mode preflight + mermaid line said
  exit 2 for a missing HMAC secret, while the summary table / CI tip said
  exit 1. `forgelm verify-audit` only ever returns 0 or 1
  (forgelm/cli/subcommands/_verify_audit.py), so normalised every reference
  to "1 = operator-actionable failure".
- cli.md (EN+TR): exit-code-2 row cited `audit warnings (with --strict)`,
  but `--strict` is not in the audit parser (the same page already says so).
  Removed the stale fragment, leaving the legitimate doctor probe-crash case.
- exit-codes.md (EN+TR): the "When to use each exit code" scenario rows for
  gate regressions read as unconditional exit 3; made them conditional on
  `evaluation.auto_revert` to match the main table + "exit 0 guarantees".
- json-output.md (EN+TR): safety-eval exit-2 wording said the
  --probes/--default-probes pair was "conflicting" yet "both required";
  the parser uses a required mutually-exclusive group, so reworded to
  "exactly one is required; neither or both -> argparse exit 2".

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

Outside-diff and nitpick findings on the docs touched by PR #63:

- configuration.md (EN+TR reference): escape the `<\|im_start\|>` pipes in
  the chatml table cell (MD056) and quote the blank line between the two
  evaluation blockquotes (MD028).
- webhooks.md (EN+TR): the WebhookConfig gate table omitted the new
  pipeline.* events; aligned notify_on_start/success/failure rows with the
  canonical event table on the same page.
- gguf-export.md (EN+TR) + troubleshooting.md/-tr.md: surround fenced code
  blocks with blank lines (MD031).
- audit-log.md (TR usermanual): point the absolute GitHub catalog links at
  the Turkish mirror audit_event_catalog-tr.md.
- audit_event_catalog-tr.md: fix mismatched link label (text said the EN
  catalog, target was the TR mirror).
- ingestion-tr.md: the Phase 15 archive link pointed at the un-suffixed EN
  sibling; no TR mirror exists, so converted it to an absolute HTTPS link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sh (PR#63-review)

The "Verify --ignore target exists" step hard-failed on the existence of
tests/test_cost_estimation.py and referenced an --ignore flag that no longer
exists. Drift-sensitive tests are now excluded marker-driven via
`pytest -m 'not fixture_drift'` (F-P8-C-20), which the very next step's
comment already declares the single source of truth. The filename guard
would cause false release failures after a harmless test-file rename. Removed
it; the marker on tests/test_cost_estimation.py (registered in pyproject.toml)
keeps the exclusion functional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ort (PR#63-review)

The guard's bare `import tomllib` is stdlib only on 3.11+, so the 3.10
CI-matrix runner failed at fixture-import time with ModuleNotFoundError,
blocking the whole test module. Fall back to the tomli backport with an
explicit ImportError + install hint (no silent fallback), add tomli to the
dev extra scoped to python_version < 3.11, and pin a regression test that the
bound tomllib exposes a working loads() on every supported runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#63-review)

- test_chat.py: replace try/except ImportError rich probe with an explicit
  importlib.util.find_spec check (coding.md: no silent import fallback).
- test_cache_subcommands.py: assert the --help subprocess exited 0 before
  validating its help text, so a broken invocation can't pass on stale output.
- test_usermanual_reference_consistency.py: assert argparse usage errors are
  coupled with exit code 2, not the bare 'argparse' token.
- test_config_doc_parity.py: qualify the benchmark output_dir field to its
  'evaluation.benchmark' heading context so an unrelated output_dir mention
  cannot satisfy the gate.

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

The PR#63-review commit 22a3924 replaced the try/except rich.markup probe
with `find_spec("rich.markup")`. But find_spec on a *submodule* imports the
parent package to locate it, so when the optional `rich` extra is absent it
raises `ModuleNotFoundError: No module named 'rich'` instead of returning
None — breaking test collection on every no-extras CI job (3.10–3.13).

Probe the top-level `rich` package instead: find_spec("rich") returns None
when absent (no parent to import) and never raises. `rich.markup` ships with
`rich`, so the top-level probe is equivalent for the True-path assertion.

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

TestCallApiJudgeNarrowing patched forgelm._http.requests.post, but
safe_post's default SSRF-pinned path issues the request via
_pinned_session().post and only falls back to requests.post on the
allow_private legacy path. So the patch leaked through to a real DNS +
HTTPS call to the default judge endpoint, which returned a real HTTP 401
and raised JudgeAuthError — failing all three narrowing tests on every CI
matrix job once the runner had outbound network reachability (they passed
before only because the runner could not reach the endpoint, so the call
was narrowed to a connection error by accident).

Patch the safe_post chokepoint instead: hermetic (no DNS, no socket) and
the correct seam — it exercises exactly _call_api_judge's response /
exception narrowing while implicitly asserting the judge routes outbound
HTTP through the chokepoint.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (7)
docs/reference/audit_event_catalog.md (1)

150-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align the pipeline.completed webhook payload contract in both mirrors.

These sections still disagree on the payload shape: the table says pipeline.completed includes final_status and stopped_at, while the schema block below only declares status, metrics, reason, and model_path, and elsewhere uses stopped_at_stage. Pick one field set and mirror it in both docs.

  • docs/reference/audit_event_catalog.md#L150-L175: make the row and schema use the same keys; rename stopped_at if stopped_at_stage is the real field.
  • docs/reference/audit_event_catalog-tr.md#L150-L175: mirror the same key set in the Turkish copy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/audit_event_catalog.md` around lines 150 - 175, The
`pipeline.completed` event documentation has conflicting payload field
definitions between the table and the JSON schema block in
docs/reference/audit_event_catalog.md: the table lists `final_status` and
`stopped_at` while the schema only includes `status`, `metrics`, `reason`, and
`model_path`, and elsewhere references `stopped_at_stage`. Determine the actual
payload contract for `pipeline.completed` (verify whether the real field is
`stopped_at` or `stopped_at_stage`, and whether `final_status` exists), then
update both the table row at lines 150-175 and the JSON schema block below it to
declare the same field set consistently. Apply the identical field set changes
to the corresponding sections in docs/reference/audit_event_catalog-tr.md at
lines 150-175 to keep the Turkish documentation in sync.
docs/standards/logging-observability.md (1)

143-173: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split the webhook contract or make the aliasing explicit.

The new table is no longer one-to-one: training.success and pipeline.completed both reference pipeline.completed, but the intro still says each webhook mirrors a distinct audit event. The generic payload schema also omits stage_count, final_status, stopped_at, and stage_name, so consumers can't validate the pipeline events from this page alone. Please either separate the pipeline family into its own schema section or update the wording to reflect the shared audit identifier and extra fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/standards/logging-observability.md` around lines 143 - 173, The
documentation currently claims each webhook event mirrors a distinct audit
event, but both `training.success` and `pipeline.completed` reference the same
audit event `pipeline.completed`, violating that contract. Additionally, the
generic "Payload schema (every event)" omits pipeline-specific fields like
`stage_count`, `final_status`, `stopped_at`, and `stage_name`, making the schema
incomplete for pipeline events. Fix this by either: (1) creating a separate
"Pipeline Events Payload Schema" section that documents the pipeline-specific
fields in addition to the common fields, or (2) updating the introductory text
to explicitly acknowledge the shared audit identifier between `training.success`
and `pipeline.completed`, then expand the generic payload schema to document all
possible fields and clarify which fields apply to which event types. Choose the
approach that best maintains clarity for webhook consumers.
docs/usermanuals/en/compliance/verify-audit.md (1)

43-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align the example log path with the documented on-disk contract.

Examples here still use .../compliance/audit_log.jsonl, but the audit-log manual documents <output_dir>/audit_log.jsonl (top-level). This mismatch can produce copy-paste file-not-found failures. Please update command and manifest-path examples to the same top-level location.

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usermanuals/en/compliance/verify-audit.md` around lines 43 - 65, Update
all file path references in the verify-audit command examples to use the correct
top-level location instead of the nested path. Replace all instances of
checkpoints/run/compliance/audit_log.jsonl with the documented contract location
of <output_dir>/audit_log.jsonl (or a more concrete example like audit_log.jsonl
if that's the preferred format). This includes the three command examples shown
in the diff section (the basic verify-audit example, the HMAC-authenticated
example, and the strict mode example with --require-hmac flag), and also fix the
additional instances referenced at lines 95-96 to maintain consistency across
all documented examples.
docs/usermanuals/tr/compliance/verify-audit.md (1)

43-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align example audit-log paths with the documented on-disk location.

The quick-start/strict-mode examples still use .../compliance/audit_log.jsonl, but the audit-log contract in this PR documents <output_dir>/audit_log.jsonl at top level. This inconsistency will cause copy-paste command failures.

Suggested doc fix
-$ forgelm verify-audit checkpoints/run/compliance/audit_log.jsonl
+$ forgelm verify-audit checkpoints/run/audit_log.jsonl
 ...
-$ FORGELM_AUDIT_SECRET="$(cat /run/secrets/audit-secret)" \
-    forgelm verify-audit checkpoints/run/compliance/audit_log.jsonl
+$ FORGELM_AUDIT_SECRET="$(cat /run/secrets/audit-secret)" \
+    forgelm verify-audit checkpoints/run/audit_log.jsonl
 ...
-$ FORGELM_AUDIT_SECRET="$(cat /run/secrets/audit-secret)" \
-    forgelm verify-audit --require-hmac \
-        checkpoints/run/compliance/audit_log.jsonl
+$ FORGELM_AUDIT_SECRET="$(cat /run/secrets/audit-secret)" \
+    forgelm verify-audit --require-hmac \
+        checkpoints/run/audit_log.jsonl
 ...
-FAIL: manifest present but unreadable at 'checkpoints/run/compliance/audit_log.jsonl.manifest.json': …
+FAIL: manifest present but unreadable at 'checkpoints/run/audit_log.jsonl.manifest.json': …

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usermanuals/tr/compliance/verify-audit.md` around lines 43 - 65, The
example commands for forgelm verify-audit use paths like
checkpoints/run/compliance/audit_log.jsonl, but this conflicts with the
documented on-disk location which specifies the audit log file should be at the
top level of the output directory as <output_dir>/audit_log.jsonl. Update all
instances of the audit-log path in the example commands (in the quick-start
section and the strict-mode section with --require-hmac flag) to use the correct
top-level path pattern that matches the documented audit-log contract, ensuring
copy-paste commands will work correctly with the actual file locations.
forgelm/merging.py (1)

295-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align per-key adapter weights with the filtered delta list.

At Line 296, deltas is filtered per key, but Line 301/Line 303 still pass the full weights vector. If any adapter is missing that key, zip-based weighting in the tensor helpers can pair deltas with the wrong adapter weights (or drop later weights), producing biased merges.

Suggested fix
-    for key in task_vectors[0]:
-        deltas = [tv[key].float() for tv in task_vectors if key in tv]
+    for key in task_vectors[0]:
+        indexed = [(idx, tv[key].float()) for idx, tv in enumerate(task_vectors) if key in tv]
+        deltas = [delta for _, delta in indexed]
         if not deltas:
             continue
+        key_weights = [weights[idx] for idx, _ in indexed]
+        key_weight_sum = sum(key_weights)
+        if key_weight_sum == 0:
+            raise ValueError(f"Adapter weights for parameter '{key}' sum to 0.")
+        key_weights = [w / key_weight_sum for w in key_weights]
 
         if method == "ties":
-            merged_delta[key] = _ties_merge_tensor(deltas, weights, trim_fraction=ties_trim_fraction)
+            merged_delta[key] = _ties_merge_tensor(deltas, key_weights, trim_fraction=ties_trim_fraction)
         elif method == "dare":
-            merged_delta[key] = _dare_merge_tensor(deltas, weights, drop_rate=dare_drop_rate, seed=dare_seed)
+            merged_delta[key] = _dare_merge_tensor(deltas, key_weights, drop_rate=dare_drop_rate, seed=dare_seed)
         else:
-            merged_delta[key] = sum(d * w for d, w in zip(deltas, weights))
+            merged_delta[key] = sum(d * w for d, w in zip(deltas, key_weights))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/merging.py` around lines 295 - 303, The deltas list is filtered at
line 296 to only include tensors from task_vectors that contain the key, but the
full weights vector is still passed to _ties_merge_tensor and _dare_merge_tensor
functions at lines 301 and 303. This creates a mismatch when some adapters lack
a particular key, causing the filtered deltas to be paired with incorrect or
misaligned weights. Filter the weights list to match the indices of task_vectors
that actually contain the key, and pass this filtered weights list to both merge
function calls instead of the full weights vector.
docs/usermanuals/tr/reference/json-output.md (1)

147-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep run_id / config_hash consistent across all training-result envelopes.

The table says these fields are always present, but the awaiting-approval and reverted examples omit them. Either add them to those envelopes or narrow the table to the promote-success case; otherwise consumers get conflicting schema guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usermanuals/tr/reference/json-output.md` around lines 147 - 181, The
awaiting-approval and reverted envelope examples in the JSON output
documentation are missing the run_id and config_hash fields, but the table
states these fields are always present when the trainer produces output. Add
run_id and config_hash fields to both the "Onay-bekliyor envelope'u" (awaiting
approval) example and the "Revert envelope'u" (reverted) example with
appropriate values to match the schema described in the table, ensuring
consistency across all training-result envelope examples shown.
notebooks/post_training_workflow.ipynb (1)

39-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align the downloaded config tag with the pinned ForgeLM version.

Line 39 installs forgelm==0.7.0, but Lines 40-43 still fetch config_template.yaml from v0.5.7. This can cause schema/field drift and break the subsequent forgelm --config ... steps.

Suggested fix
-    "# Download config_template.yaml from the v0.5.7 tag (NOT main) so the YAML\n",
+    "# Download config_template.yaml from the v0.7.0 tag (NOT main) so the YAML\n",
@@
-    "!wget -q https://raw.githubusercontent.com/HodeTech/ForgeLM/v0.5.7/config_template.yaml\n",
+    "!wget -q https://raw.githubusercontent.com/HodeTech/ForgeLM/v0.7.0/config_template.yaml\n",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/post_training_workflow.ipynb` around lines 39 - 43, The pip install
command on line 39 installs forgelm version 0.7.0, but the wget command that
downloads config_template.yaml is fetching from the v0.5.7 tag, creating a
version mismatch that can cause schema/field drift issues. Update the wget URL
to fetch the config_template.yaml from the v0.7.0 tag instead of v0.5.7 to align
with the pinned forgelm version being installed, ensuring the downloaded
configuration is compatible with the installed wheel.
♻️ Duplicate comments (1)
forgelm/compliance.py (1)

294-301: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move audit re-root bypass to validated config, not env var.

Line 294 still uses FORGELM_ALLOW_AUDIT_REROOT as a behavior switch for a tamper-sensitive integrity gate, which keeps this path outside validated config/audit contracts.

As per coding guidelines, “Behaviour is determined by validated YAML config; do not sniff env-vars for behaviour (env-vars for secrets only).” Based on learnings, this same repo rule is explicitly reiterated.

Suggested direction
-class AuditLogger:
-    def __init__(self, output_dir: str, run_id: Optional[str] = None):
+class AuditLogger:
+    def __init__(
+        self,
+        output_dir: str,
+        run_id: Optional[str] = None,
+        *,
+        allow_audit_reroot: bool = False,
+    ):
         ...
+        self._allow_audit_reroot = allow_audit_reroot
...
-            if os.getenv("FORGELM_ALLOW_AUDIT_REROOT") != "1":
+            if not self._allow_audit_reroot:
                 raise ConfigError(...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/compliance.py` around lines 294 - 301, The audit re-root bypass check
at line 294 in compliance.py currently uses the environment variable
FORGELM_ALLOW_AUDIT_REROOT to control behavior, but per coding guidelines,
behavior must be determined by validated YAML config, not environment variables.
Replace the os.getenv("FORGELM_ALLOW_AUDIT_REROOT") check with a corresponding
validated configuration parameter from the config object, ensuring the audit
re-root bypass is controlled through the config layer rather than environment
variables (which should be reserved for secrets only).

Sources: Coding guidelines, Learnings

🟡 Minor comments (17)
docs/guides/ingestion-tr.md-493-493 (1)

493-493: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid pinning this TR guide to main with an absolute GitHub URL.

Line 493 links to .../blob/main/..., which can drift from versioned docs and diverges from the EN mirror’s relative-link style. Use a relative repo link here too.

Suggested fix
-> [Faz 15 arşivi](https://github.com/HodeTech/ForgeLM/blob/main/docs/roadmap/completed-phases.md#phase-15--ingestion-pipeline-reliability-v060)
+> [Faz 15 arşivi](../roadmap/completed-phases.md#phase-15--ingestion-pipeline-reliability-v060)

As per coding guidelines, bilingual documentation mirrors should stay synchronized and stable across EN↔TR docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/ingestion-tr.md` at line 493, The link on line 493 in
docs/guides/ingestion-tr.md uses an absolute GitHub URL with blob/main that can
drift from versioned documentation. Replace this absolute URL with a relative
repository link to match the style used in the English mirror guide, ensuring
the Turkish and English documentation mirrors remain synchronized and stable.

Source: Coding guidelines

forgelm/model_card.py-252-260 (1)

252-260: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use context-specific YAML serialization for front-matter instead of reusing markdown-sanitized text.

base_model is still emitted as a plain YAML scalar in front-matter. Quote-led or YAML-significant inputs can still produce malformed metadata even after markdown-char stripping.

💡 Proposed fix
 MODEL_CARD_TEMPLATE = """---
 language:
 - en
 tags:
 - forgelm
 - fine-tuned
 - lora
 {extra_tags}
-base_model: {base_model}
+base_model: {base_model_yaml}
 ---
@@
     base_model = _neutralize_md_inline(config.model.name_or_path)
@@
+    # YAML front-matter needs YAML-safe scalar serialization (separate from
+    # markdown inline sanitization).
+    base_model_yaml = yaml.safe_dump(base_model, default_flow_style=True).strip()
+
     card = MODEL_CARD_TEMPLATE.format(
         model_name=model_name,
         base_model=base_model,
+        base_model_yaml=base_model_yaml,
         backend=config.model.backend,

Also applies to: 262-264, 275-275, 281-281

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/model_card.py` around lines 252 - 260, The variables base_model,
dataset, model_path, and model_name are sanitized using _neutralize_md_inline()
which only handles markdown characters, but they are subsequently used in YAML
front-matter where YAML-specific syntax (quotes, colons, etc.) can still cause
injection vulnerabilities or malformed metadata. Replace the
_neutralize_md_inline() sanitization with a context-specific YAML serialization
function that properly escapes or quotes values according to YAML specification.
Apply this fix to all locations where these variables are assigned and used in
front-matter generation, including the variable assignments at lines 252-260 and
also at lines 262-264, 275, and 281 where similar YAML front-matter values are
set.
docs/reference/configuration-tr.md-102-102 (1)

102-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the canonical packing row to the Turkish mirror.

The alias note now points at packing, but this table never defines that field, so readers can't discover the canonical setting. Add the packing row above (or trim the alias note) to keep the EN/TR docs in sync. As per coding guidelines, bilingual documentation (EN ↔ TR) must be kept in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/configuration-tr.md` at line 102, The Turkish documentation
file references a canonical `packing` field as the replacement for the
deprecated `sample_packing` alias, but the `packing` row is missing from the
configuration table in that file. Add the `packing` configuration row to the
Turkish table (positioned before or near the `sample_packing` row) with the same
content and structure as exists in the English version documentation to maintain
synchronization between the EN and TR bilingual documentation. This ensures
readers can discover and understand the canonical `packing` setting that
`sample_packing` points to.

Source: Coding guidelines

tests/test_pytest_markers.py-39-42 (1)

39-42: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not silently skip unreadable test files when collecting applied markers.

Swallowing OSError here can hide real repository/readability problems and weaken this guard’s signal. Fail loudly with the file path/context instead of continuing.

As per coding guidelines, “No silent failures: avoid ... logging-and-swallowing for non-fatal paths.”

Proposed fix
-        except OSError:
-            continue
+        except OSError as exc:
+            pytest.fail(f"failed to read marker source file '{path}': {exc}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_pytest_markers.py` around lines 39 - 42, The try-except block
around path.read_text(encoding="utf-8") silently catches OSError and continues,
which masks real file access problems. Instead of using continue in the except
OSError clause, raise an error that includes the file path and the original
exception details to fail loudly and provide proper context for debugging. This
ensures that repository or readability issues are not hidden from the guard's
signal.

Source: Coding guidelines

tests/test_pytest_markers.py-59-61 (1)

59-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parse pytest addopts from TOML instead of substring-matching the full file text.

The current assertion can false-pass if "--strict-markers" appears outside addopts (e.g., comments or docs). Read tool.pytest.ini_options.addopts and assert token presence there.

Proposed fix
 def test_strict_markers_enabled():
-    addopts = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
-    assert "--strict-markers" in addopts, (
+    data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
+    addopts = data["tool"]["pytest"]["ini_options"].get("addopts", "")
+    assert "--strict-markers" in addopts, (
         "--strict-markers must stay in addopts so a typo'd `@pytest.mark` is an error, not a silent no-op."
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_pytest_markers.py` around lines 59 - 61, The current
implementation reads the entire pyproject.toml file as text and performs a
substring search for "--strict-markers", which can produce false positives if
the string appears in comments or other sections. Replace the text-based read
with proper TOML parsing: parse the pyproject.toml file using a TOML parser,
extract the addopts value from tool.pytest.ini_options.addopts, and then assert
that "--strict-markers" is present in that parsed addopts configuration instead
of searching the raw file text.
tests/test_guard_wiring.py-47-47 (1)

47-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include .yaml workflows in the guard scan.

Line 47 only scans *.yml. A guard wired in a .yaml workflow would be treated as unwired and can trigger false CI failures. Scan both extensions in _workflow_text().

Proposed fix
 def _workflow_text() -> str:
-    return "\n".join(p.read_text(encoding="utf-8") for p in sorted(_WORKFLOWS.glob("*.yml")))
+    files = sorted(_WORKFLOWS.glob("*.yml")) + sorted(_WORKFLOWS.glob("*.yaml"))
+    return "\n".join(p.read_text(encoding="utf-8") for p in files)

As per coding guidelines, workflow policy explicitly covers both .yml and .yaml files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_guard_wiring.py` at line 47, The _workflow_text() function on line
47 only scans workflow files with the .yml extension using
_WORKFLOWS.glob("*.yml"), but it should also include files with the .yaml
extension to prevent false CI failures. Modify the glob pattern to match both
.yml and .yaml files by using two separate glob calls (one for "*.yml" and one
for "*.yaml") and combine their results, or use a single glob pattern that
matches both extensions, ensuring the sorted results are properly merged.

Source: Coding guidelines

tests/test_phantom_tool_citations.py-41-48 (1)

41-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard scope excludes one declared production site.

Line 74 only scans docs/**/*.md and tools/*.py, but Line 41–48 explicitly includes .github/workflows/ci.yml in the protected scope. That leaves a regression hole for the exact citation this test claims to guard.

Suggested patch
-    for path in (*_REPO_ROOT.glob("docs/**/*.md"), *_REPO_ROOT.glob("tools/*.py")):
+    for path in (
+        *_REPO_ROOT.glob("docs/**/*.md"),
+        *_REPO_ROOT.glob("tools/*.py"),
+        _REPO_ROOT / ".github" / "workflows" / "ci.yml",
+    ):
+        if not path.is_file():
+            continue
         if _is_under_gitignored_doc_dir(path):
             continue

Also applies to: 74-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_phantom_tool_citations.py` around lines 41 - 48, The
_PRODUCTION_SITES tuple at lines 41-48 correctly declares
.github/workflows/ci.yml as a protected production site, but the scan logic at
lines 74-86 only covers docs/**/*.md and tools/*.py patterns, creating a gap
where the ci.yml file is not actually guarded. Update the scan pattern at lines
74-86 to also include .github/workflows/*.yml or a similar pattern that will
match .github/workflows/ci.yml, ensuring the scan scope matches all declared
production sites in _PRODUCTION_SITES.
docs/roadmap/completed-phases.md-921-927 (1)

921-927: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the blank line inside the blockquote (MD028).

Line 927 introduces a blank quoted line, which triggers markdownlint MD028 (“no-blanks-blockquote”). Keep the blockquote contiguous to avoid docs-lint noise/failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/roadmap/completed-phases.md` around lines 921 - 927, A blank line within
the blockquote on line 927 in the completed-phases.md file violates markdownlint
rule MD028 (no-blanks-blockquote). Remove the empty quoted line to make the
blockquote contiguous, ensuring all lines starting with > are consecutive
without any blank lines in between.

Source: Linters/SAST tools

tests/test_config_doc_parity.py-43-47 (1)

43-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The benchmark output_dir guard can false-pass.

This assertion only requires `evaluation.benchmark` and `output_dir` anywhere in the document, so it can pass even if output_dir is documented in another section. Scope the check to the benchmark section block to avoid silent regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_config_doc_parity.py` around lines 43 - 47, The assertion checking
for the field "evaluation.benchmark.output_dir" currently searches for both the
"`evaluation.benchmark`" and "`output_dir`" strings anywhere in the entire text
variable, which can cause false positives if `output_dir` exists in an unrelated
section. Refactor the assertion to first locate the benchmark section block (the
text region between the "`evaluation.benchmark`" heading and the next section),
then verify that "`output_dir`" exists specifically within that scoped section
rather than in the full document text.
docs/standards/testing.md-57-60 (1)

57-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep fixture ownership wording consistent with the layout tree.

This section correctly says the canonical minimal_config factory lives in tests/_helpers/factories.py, but the tree still labels tests/conftest.py as the factory location. Please update the tree note so the guidance is not contradictory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/standards/testing.md` around lines 57 - 60, The testing.md documentation
contains contradictory information: the narrative text correctly states that the
canonical minimal_config factory lives in tests/_helpers/factories.py and is
re-exported via tests/conftest.py, but the directory tree diagram in this
section still incorrectly labels tests/conftest.py as the factory location.
Update the tree structure note to accurately reflect that
tests/_helpers/factories.py is the canonical factory location, with
tests/conftest.py being the re-export point, so the documentation guidance is
consistent throughout.
docs/usermanuals/en/evaluation/judge.md-63-63 (1)

63-63: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the page’s failure semantics in one direction.

This paragraph correctly documents the auto_revert split, but the earlier intro still says the run fails below min_score unconditionally. Please reconcile that wording so the page exposes one contract.

As per coding guidelines, exit codes are a public contract and should be documented consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usermanuals/en/evaluation/judge.md` at line 63, The documentation has
inconsistent descriptions of failure semantics when mean_score falls below
min_score. The section at line 63 correctly documents that the outcome depends
on the auto_revert parameter (either reverting with exit code 3, or promoting
with exit code 0), but an earlier intro section in the same file states
unconditionally that the run fails below min_score. Locate the earlier intro
section and update its wording to accurately reflect that the actual behavior is
conditional on the auto_revert setting. Ensure both sections consistently
document the public contract for exit codes and whether the model gets promoted
or reverted, so readers understand that a judge gate failure does not
automatically result in model reversion or a non-zero exit code without
auto_revert: true.

Source: Coding guidelines

forgelm/webhook.py-122-127 (1)

122-127: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the HTTP warning accurate when require_https=true.

Line 126 can refuse http:// delivery, but _send still logs “Data will be sent unencrypted.” in that path. This message is incorrect when delivery is actually blocked.

Suggested fix
-        if url.startswith("http://"):  # NOSONAR python:S5332
-            logger.warning("Webhook URL uses HTTP (not HTTPS). Data will be sent unencrypted.")
+        if url.startswith("http://"):  # NOSONAR python:S5332
+            if bool(getattr(self.config, "require_https", False)):
+                logger.warning(
+                    "Webhook URL uses HTTP (not HTTPS) and require_https=true; delivery will be refused."
+                )
+            else:
+                logger.warning("Webhook URL uses HTTP (not HTTPS). Data will be sent unencrypted.")

As per coding guidelines, logging must follow docs/standards/logging-observability.md and remain operationally accurate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/webhook.py` around lines 122 - 127, The `_send` method logs "Data
will be sent unencrypted." regardless of whether HTTP delivery is actually
allowed. When `require_https=true`, the allow_insecure_http parameter becomes
False (line 126), which blocks HTTP delivery, but the warning in `_send` should
not be logged in this case since data is not actually being sent unencrypted.
Update the logging logic in `_send` to only warn about unencrypted transmission
when allow_insecure_http is True and the destination URL is actually using the
http:// scheme, ensuring the warning is operationally accurate and only appears
when data will genuinely be sent unencrypted.

Source: Coding guidelines

docs/usermanuals/tr/operations/webhooks.md-37-38 (1)

37-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the Turkish audit-event mirror here.

Both references still send Turkish readers to the English audit_event_catalog.md. Please switch them to the Turkish mirror so this page stays localized end-to-end. As per coding guidelines, Turkish docs should not link to English siblings when a Turkish version exists.

Also applies to: 67-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usermanuals/tr/operations/webhooks.md` around lines 37 - 38, Update the
GitHub links in the Turkish webhook documentation to reference the Turkish
mirror of the audit event catalog instead of the English version. In the file
docs/usermanuals/tr/operations/webhooks.md, replace the link at lines 37-38 that
currently points to the English audit_event_catalog.md with a reference to the
Turkish audit catalog mirror. Apply the same fix at lines 67-68 where the same
English audit catalog link appears. This ensures the Turkish documentation stays
fully localized and follows the guideline that Turkish docs should not link to
English siblings when a translated version exists.

Source: Coding guidelines

notebooks/safety_evaluation.ipynb-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor

Keep notebook version pins consistent across install and fallback download paths.

Lines 25-26 pin ForgeLM to 0.7.0, but the notebook fetches prompts from v0.5.7. This mismatch can silently load incompatible assets in a single run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/safety_evaluation.ipynb` around lines 25 - 26, The pip install
command pins ForgeLM to version 0.7.0, but the notebook contains a fallback
download path that fetches prompts from v0.5.7, creating a version mismatch that
can load incompatible assets. Locate where the notebook references or fetches
assets from v0.5.7 (likely in a URL or download path for prompts) and update
that version reference to 0.7.0 to match the pinned installation version,
ensuring all asset sources within the notebook use the same ForgeLM version.
tests/test_pypdf_normalise.py-65-79 (1)

65-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the docstring assertion semantic instead of markdown-fragment specific.

Line 78 checks the literal fragment "not** 1:1", which is brittle to harmless emphasis/formatting edits. Prefer asserting intent-level tokens so the test protects meaning, not exact markdown punctuation.

Suggested test hardening
-        assert "not** 1:1" in doc
+        assert "1:1" in doc
+        assert "lossy" in doc.lower()
+        assert "not" in doc.lower()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_pypdf_normalise.py` around lines 65 - 79, The assertion on line 78
checks for the literal string fragment "not** 1:1" which includes markdown
formatting characters (the double asterisks). This test is brittle because it
depends on specific markdown punctuation rather than semantic meaning. Instead
of asserting the exact markdown fragment "not** 1:1", modify the
test_docstring_acknowledges_multichar_rule_is_not_1to1 method to check for the
semantic intent — that the docstring contains tokens indicating the
transformation is NOT purely 1:1 — without requiring specific markdown
formatting. This could be done by checking for separate meaningful phrases like
"not" and "1:1" or other variations that verify the core meaning while remaining
resilient to harmless formatting changes.
notebooks/data_curation.ipynb-25-30 (1)

25-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the explanatory version note with the pinned package version.

Line 26 still references a v0.5.5 closure-cycle bundle while the install command now pins forgelm[ingestion]==0.7.0. Updating that sentence to match the new pin will avoid reader confusion.

Suggested patch
-    "# Pinned to v0.7.0 (the v0.5.5 closure-cycle bundle ships Phases 11+11.5+12+12.5\n",
+    "# Pinned to v0.7.0 (this release bundle ships Phases 11+11.5+12+12.5\n",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/data_curation.ipynb` around lines 25 - 30, The explanatory comment
references v0.5.5 but the pip install command pins forgelm[ingestion]==0.7.0.
Update the comment text (specifically the sentence mentioning "the v0.5.5
closure-cycle bundle") to align with the actual pinned version v0.7.0 so the
documentation accurately reflects what version and features are being installed.
forgelm/cli/subcommands/_purge.py-122-124 (1)

122-124: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Enforce the configured max length after adding the truncation suffix.

At Line 124, the return value can exceed _AUDIT_ERROR_MESSAGE_MAX because the suffix is appended after slicing to max length.

💡 Suggested fix
-    if len(masked) <= _AUDIT_ERROR_MESSAGE_MAX:
-        return masked
-    return masked[:_AUDIT_ERROR_MESSAGE_MAX] + "…[truncated]"
+    if len(masked) <= _AUDIT_ERROR_MESSAGE_MAX:
+        return masked
+    suffix = "…[truncated]"
+    head = _AUDIT_ERROR_MESSAGE_MAX - len(suffix)
+    if head <= 0:
+        return suffix[:_AUDIT_ERROR_MESSAGE_MAX]
+    return masked[:head] + suffix
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/cli/subcommands/_purge.py` around lines 122 - 124, The truncation
logic at line 124 appends the suffix "…[truncated]" after slicing to
_AUDIT_ERROR_MESSAGE_MAX, which causes the final return value to exceed the
configured maximum length. Fix this by accounting for the suffix length when
slicing the masked string. Calculate the appropriate slice endpoint by
subtracting the length of the truncation suffix from _AUDIT_ERROR_MESSAGE_MAX,
then slice the masked string to that adjusted length before appending the suffix
to ensure the total length never exceeds the configured maximum.
🧹 Nitpick comments (2)
forgelm/__init__.py (1)

302-305: ⚡ Quick win

Preserve the documented tier order in __dir__.

sorted(set(__all__)) dedupes, but it also destroys the Stable/Experimental ordering this module says dir(forgelm) should preserve. A stable-order dedupe (dict.fromkeys(__all__)) would keep the advertised grouping intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forgelm/__init__.py` around lines 302 - 305, The return statement in the
`__dir__` function uses `sorted(set(__all__))` which sorts items alphabetically
and destroys the documented Stable/Experimental tier ordering. Replace this with
`dict.fromkeys(__all__)` to preserve the original insertion order while still
deduplicating items, maintaining the advertised tier grouping that users expect
when calling `dir(forgelm)`.
tests/test_human_approval_gate.py (1)

147-164: 🏗️ Heavy lift

test_production_staging_path_carries_run_id_suffix is currently self-referential.

This test recomputes the expected string locally instead of asserting a value produced by the runtime path-construction flow, so it can pass even if production staging-path construction drifts. Please assert against the actual path emitted by the trainer execution path that builds staging names.

Based on learnings, new capability coverage should verify real behavior paths, not mirrored formulas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_human_approval_gate.py` around lines 147 - 164, The test
test_production_staging_path_carries_run_id_suffix currently recomputes the gate
path locally using the same formula
(f"{final_path}.staging.{trainer.audit.run_id}") that production code would use,
making it self-referential and unable to catch if production path construction
changes. Instead of computing gate_path locally, obtain the actual staging path
that the trainer object produces during execution (likely from the trainer's
state, audit object, or output directory artifacts), and assert against that
real path value to ensure the test catches any drift in actual production path
construction behavior.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07e95c9f-9e83-4eba-a697-4d133ff3c945

📥 Commits

Reviewing files that changed from the base of the PR and between 402bb05 and 22a3924.

📒 Files selected for processing (261)
  • .agents/skills/cut-release/SKILL.md
  • .claude/skills/cut-release/SKILL.md
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • config_template.yaml
  • docs/design/blackwell_optimized.md
  • docs/design/iso27001_soc2_alignment.md
  • docs/design/library_api.md
  • docs/guides/air_gap_deployment-tr.md
  • docs/guides/alignment-tr.md
  • docs/guides/alignment.md
  • docs/guides/gdpr_erasure-tr.md
  • docs/guides/getting-started-tr.md
  • docs/guides/human_approval_gate-tr.md
  • docs/guides/ingestion-tr.md
  • docs/guides/ingestion.md
  • docs/guides/library_api-tr.md
  • docs/guides/library_api.md
  • docs/guides/performance-tr.md
  • docs/guides/pipeline-tr.md
  • docs/guides/pipeline.md
  • docs/guides/troubleshooting-tr.md
  • docs/guides/troubleshooting.md
  • docs/qms/README-tr.md
  • docs/qms/sop_change_management-tr.md
  • docs/qms/sop_change_management.md
  • docs/qms/sop_data_management-tr.md
  • docs/qms/sop_data_management.md
  • docs/reference/approvals_subcommand-tr.md
  • docs/reference/approve_subcommand-tr.md
  • docs/reference/audit_event_catalog-tr.md
  • docs/reference/audit_event_catalog.md
  • docs/reference/configuration-tr.md
  • docs/reference/configuration.md
  • docs/reference/doctor_subcommand-tr.md
  • docs/reference/library_api_reference-tr.md
  • docs/reference/library_api_reference.md
  • docs/reference/purge_subcommand-tr.md
  • docs/reference/reverse_pii_subcommand-tr.md
  • docs/reference/reverse_pii_subcommand.md
  • docs/reference/supply_chain_security-tr.md
  • docs/reference/supply_chain_security.md
  • docs/reference/usage-tr.md
  • docs/reference/usage.md
  • docs/reference/verify_annex_iv_subcommand-tr.md
  • docs/reference/verify_audit-tr.md
  • docs/reference/verify_gguf_subcommand-tr.md
  • docs/reference/verify_integrity_subcommand-tr.md
  • docs/reference/verify_integrity_subcommand.md
  • docs/roadmap-tr.md
  • docs/roadmap.md
  • docs/roadmap/completed-phases.md
  • docs/roadmap/phase-14-5-pipeline-hardening.md
  • docs/roadmap/releases.md
  • docs/standards/error-handling.md
  • docs/standards/localization.md
  • docs/standards/logging-observability.md
  • docs/standards/regex.md
  • docs/standards/release.md
  • docs/standards/testing.md
  • docs/usermanuals/en/compliance/audit-log.md
  • docs/usermanuals/en/compliance/verify-audit.md
  • docs/usermanuals/en/deployment/chat.md
  • docs/usermanuals/en/deployment/deploy-targets.md
  • docs/usermanuals/en/deployment/gguf-export.md
  • docs/usermanuals/en/evaluation/judge.md
  • docs/usermanuals/en/operations/gpu-cost.md
  • docs/usermanuals/en/operations/webhooks.md
  • docs/usermanuals/en/reference/cli.md
  • docs/usermanuals/en/reference/configuration.md
  • docs/usermanuals/en/reference/exit-codes.md
  • docs/usermanuals/en/reference/json-output.md
  • docs/usermanuals/en/reference/yaml-templates.md
  • docs/usermanuals/en/training/sft.md
  • docs/usermanuals/tr/compliance/audit-log.md
  • docs/usermanuals/tr/compliance/verify-audit.md
  • docs/usermanuals/tr/deployment/chat.md
  • docs/usermanuals/tr/deployment/deploy-targets.md
  • docs/usermanuals/tr/deployment/gguf-export.md
  • docs/usermanuals/tr/evaluation/judge.md
  • docs/usermanuals/tr/operations/gpu-cost.md
  • docs/usermanuals/tr/operations/webhooks.md
  • docs/usermanuals/tr/reference/cli.md
  • docs/usermanuals/tr/reference/configuration.md
  • docs/usermanuals/tr/reference/exit-codes.md
  • docs/usermanuals/tr/reference/json-output.md
  • docs/usermanuals/tr/reference/yaml-templates.md
  • docs/usermanuals/tr/training/sft.md
  • forgelm/__init__.py
  • forgelm/_http.py
  • forgelm/_pypdf_normalise.py
  • forgelm/_strip_pattern.py
  • forgelm/_version.py
  • forgelm/benchmark.py
  • forgelm/chat.py
  • forgelm/cli/__init__.py
  • forgelm/cli/_abi_check.py
  • forgelm/cli/_argparse_types.py
  • forgelm/cli/_dispatch.py
  • forgelm/cli/_dry_run.py
  • forgelm/cli/_exit_codes.py
  • forgelm/cli/_no_train_modes.py
  • forgelm/cli/_parser.py
  • forgelm/cli/_pipeline.py
  • forgelm/cli/_result.py
  • forgelm/cli/_resume.py
  • forgelm/cli/_wizard.py
  • forgelm/cli/subcommands/_approvals.py
  • forgelm/cli/subcommands/_approve.py
  • forgelm/cli/subcommands/_audit.py
  • forgelm/cli/subcommands/_cache.py
  • forgelm/cli/subcommands/_chat.py
  • forgelm/cli/subcommands/_deploy.py
  • forgelm/cli/subcommands/_doctor.py
  • forgelm/cli/subcommands/_export.py
  • forgelm/cli/subcommands/_purge.py
  • forgelm/cli/subcommands/_quickstart.py
  • forgelm/cli/subcommands/_reverse_pii.py
  • forgelm/cli/subcommands/_safety_eval.py
  • forgelm/cli/subcommands/_verify_annex_iv.py
  • forgelm/cli/subcommands/_verify_gguf.py
  • forgelm/cli/subcommands/_verify_integrity.py
  • forgelm/compliance.py
  • forgelm/config.py
  • forgelm/data.py
  • forgelm/data_audit/_pii_regex.py
  • forgelm/data_audit/_simhash.py
  • forgelm/data_audit/_splits.py
  • forgelm/data_audit/_streaming.py
  • forgelm/data_audit/_types.py
  • forgelm/deploy.py
  • forgelm/export.py
  • forgelm/fit_check.py
  • forgelm/grpo_rewards.py
  • forgelm/inference.py
  • forgelm/ingestion.py
  • forgelm/judge.py
  • forgelm/merging.py
  • forgelm/model.py
  • forgelm/model_card.py
  • forgelm/results.py
  • forgelm/safety.py
  • forgelm/synthetic.py
  • forgelm/trainer.py
  • forgelm/utils.py
  • forgelm/webhook.py
  • forgelm/wizard/__init__.py
  • forgelm/wizard/_byod.py
  • forgelm/wizard/_io.py
  • forgelm/wizard/_orchestrator.py
  • forgelm/wizard/_state.py
  • notebooks/data_curation.ipynb
  • notebooks/dpo_alignment.ipynb
  • notebooks/galore_memory_optimization.ipynb
  • notebooks/grpo_reasoning.ipynb
  • notebooks/ingestion_playground.ipynb
  • notebooks/kto_binary_feedback.ipynb
  • notebooks/multi_dataset.ipynb
  • notebooks/post_training_workflow.ipynb
  • notebooks/quickstart_sft.ipynb
  • notebooks/safety_evaluation.ipynb
  • notebooks/synthetic_data_training.ipynb
  • pyproject.toml
  • tests/_data/api_signatures_1.0.0.json
  • tests/test_approvals_listing.py
  • tests/test_benchmark.py
  • tests/test_cache_subcommands.py
  • tests/test_chat.py
  • tests/test_check_anchor_resolution.py
  • tests/test_check_bilingual_code_blocks.py
  • tests/test_check_bilingual_parity.py
  • tests/test_check_cli_help_consistency.py
  • tests/test_check_doc_numerical_claims.py
  • tests/test_check_field_descriptions.py
  • tests/test_check_http_discipline.py
  • tests/test_check_library_api_doc.py
  • tests/test_check_no_unguarded_sys_modules_pop.py
  • tests/test_check_notebook_pins.py
  • tests/test_check_pip_audit.py
  • tests/test_check_site_chrome_parity.py
  • tests/test_check_tr_links_prefer_mirror.py
  • tests/test_check_wizard_defaults_sync.py
  • tests/test_check_yaml_snippets.py
  • tests/test_cli.py
  • tests/test_cli_helpers.py
  • tests/test_cli_phase10.py
  • tests/test_cli_quickstart_wiring.py
  • tests/test_cli_subcommands.py
  • tests/test_cli_training_seam.py
  • tests/test_compliance.py
  • tests/test_config.py
  • tests/test_config_doc_parity.py
  • tests/test_cost_estimation.py
  • tests/test_data.py
  • tests/test_data_audit.py
  • tests/test_data_edge_cases.py
  • tests/test_deploy.py
  • tests/test_distributed.py
  • tests/test_doctor.py
  • tests/test_eu_ai_act.py
  • tests/test_fit_check.py
  • tests/test_gdpr_erasure.py
  • tests/test_grpo_format_reward.py
  • tests/test_grpo_math_reward.py
  • tests/test_guard_wiring.py
  • tests/test_http_dns_rebinding.py
  • tests/test_human_approval_gate.py
  • tests/test_inference.py
  • tests/test_ingestion_reliability.py
  • tests/test_judge_functions.py
  • tests/test_library_api.py
  • tests/test_long_context.py
  • tests/test_merging_algos.py
  • tests/test_model.py
  • tests/test_model_card.py
  • tests/test_model_loading_dispatch.py
  • tests/test_no_train_modes.py
  • tests/test_oom_recovery.py
  • tests/test_phantom_tool_citations.py
  • tests/test_phase12_review_fixes.py
  • tests/test_phase7.py
  • tests/test_pipeline_compliance.py
  • tests/test_pipeline_config.py
  • tests/test_pipeline_fixtures.py
  • tests/test_pipeline_orchestrator.py
  • tests/test_pypdf_normalise.py
  • tests/test_pytest_markers.py
  • tests/test_quickstart.py
  • tests/test_quickstart_subprocess.py
  • tests/test_reverse_pii.py
  • tests/test_safety_advanced.py
  • tests/test_strip_pattern.py
  • tests/test_supply_chain_security.py
  • tests/test_synthetic.py
  • tests/test_trainer.py
  • tests/test_trainer_sft_config.py
  • tests/test_usermanual_reference_consistency.py
  • tests/test_utils.py
  • tests/test_verification_toolbelt.py
  • tests/test_webhook.py
  • tests/test_wizard_byod.py
  • tests/test_wizard_phase22.py
  • tools/check_anchor_resolution.py
  • tools/check_bilingual_code_blocks.py
  • tools/check_bilingual_parity.py
  • tools/check_cli_help_consistency.py
  • tools/check_doc_numerical_claims.py
  • tools/check_field_descriptions.py
  • tools/check_http_discipline.py
  • tools/check_no_analysis_refs.py
  • tools/check_no_unguarded_sys_modules_pop.py
  • tools/check_notebook_pins.py
  • tools/check_pip_audit.py
  • tools/check_tr_links_prefer_mirror.py
  • tools/check_wizard_defaults_sync.py
  • tools/generate_sbom.py
  • tools/generate_wizard_defaults.py
  • tools/pip_audit_ignores.yaml

Comment thread forgelm/cli/_argparse_types.py
Comment thread forgelm/cli/_resume.py Outdated
Comment thread forgelm/cli/subcommands/_approve.py
Comment thread forgelm/cli/subcommands/_purge.py
Comment thread forgelm/cli/subcommands/_verify_integrity.py
Comment thread forgelm/compliance.py Outdated
Comment thread forgelm/data_audit/_streaming.py
Comment thread forgelm/data.py
Comment thread forgelm/judge.py
Comment thread forgelm/safety.py
cemililik and others added 11 commits June 15, 2026 12:44
…view)

merge_pipeline_stage_config dumped stage model/lora/training/data/evaluation with exclude_none only; an override omitting staging_ttl_days re-materialised the default 7 as explicitly-set and falsely raised ConfigError against a canonical retention block. Match the root dump (exclude_unset=True). CodeRabbit #12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_non_negative_float and _sampling_temperature let NaN/inf bypass bounds checks (comparisons against NaN are False), flowing a malformed value into generation config. Guard with math.isfinite so they fail at the parse boundary (exit 2). CodeRabbit #19.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#63-review)

split('-')[-1] accepted malformed names like checkpoint-bad-900 as numbered checkpoints. Slice after the 'checkpoint-' prefix and require the whole suffix isdecimal(); malformed dirs now hit the non-numeric-suffix warning. CodeRabbit #20.

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

_run_reject_cmd logged human_approval.rejected with no OSError guard while the approve path had one; a storage failure escaped as an uncaught traceback with a non-contract exit code and could still reach notify_failure. Mirror the approve guard (exit 2, before the notifier). CodeRabbit #21.

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

The escaping-path audit emit wrote raw resolved paths + run-id into the append-only chain, bypassing the mandated secret->PII->length-bound sanitiser used on the sibling OSError branches. Route it through _sanitise_audit_error_message. CodeRabbit #22.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PR#63-review)

verify_integrity iterated manifest['artifacts'] unchecked: null/int raised an uncaught TypeError (traceback, not exit 1) and str/dict false-passed as 'All 0 artifacts present' (exit 0). Add an explicit container guard returning valid=False (exit 1) rather than CodeRabbit's silent coercion. CodeRabbit #23.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… secret (PR#63-review)

#24: export_compliance_artifacts rollback swallowed os.remove/os.replace failures with except OSError: pass; collect and logger.error them before re-raising the original promotion error. #25: strict-HMAC guard rejected only None, letting an empty-string secret through; gate on 'not hmac_secret'. CodeRabbit #24,#25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_read_jsonl_split decoded each line twice (replace-decode for text, then strict-decode to infer decode_error). Switch to strict-first with an errors='replace' fallback only on UnicodeDecodeError — behaviour-identical, one decode on the common path. Existing TestJsonlDecodeErrorDetection guards both branches. CodeRabbit #26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PR#63-review)

_format_user_assistant_row gated System validation on truthiness, so 0/False/0.0/[]/{} bypassed clean_string's loud-fail contract and were silently dropped. Gate on sys_text != '' so only the genuine empty-string absent-system path skips, and every other value (incl. None) fails loudly. CodeRabbit #27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…R#63-review)

_validate_rubric let an empty positional {} placeholder slip past the unexpected/missing checks, then crashed .format() with IndexError mid-eval. Add an explicit 'if "" in field_roots' guard with an actionable message. CodeRabbit #28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lary (PR#63-review)

The uninitialised-classifier-head guard only matched the 2-label {label_0,label_1} default; a causal-LM checkpoint exposing LABEL_0..LABEL_2+ bypassed it and could emit meaningless safety verdicts. Match any all-LABEL_N vocabulary. CodeRabbit #29.

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

Copy link
Copy Markdown
Collaborator Author

CodeRabbit review-response — 30 open threads triaged

Every open CodeRabbit thread was verified against branch HEAD by an independent pass, then either fixed-with-test or skipped-with-reason. Outcome: 12 fixed, 15 already addressed, 3 intentional skips, 0 invalid. Full suite green (2775 passed) and CI green across Python 3.10–3.13.

✅ Fixed (12) — each ships a regression test

Finding Sev Commit What changed
forgelm/config.py medium 49124bf Stage-override model_dump omits exclude_unset, re-materialising staging_ttl_days=7 and causing false…
forgelm/cli/_argparse_types.py low ab68ca8 Float CLI validators accept NaN/inf (bounds checks bypassed)
forgelm/cli/_resume.py medium 357a618 checkpoint validation accepts malformed names like checkpoint-bad-900 via split("-")[-1]
forgelm/cli/subcommands/_approve.py medium 14a283a _run_reject_cmd audit.log_event lacks the OSError guard the approve path has
forgelm/cli/subcommands/_purge.py medium cf53e38 PathTraversalRefused audit error_message writes raw resolved paths + run-id, bypassing the mandated sanitizer
forgelm/cli/subcommands/_verify_integrity.py medium 02ac6e6 Non-list manifest "artifacts" value raises uncaught TypeError, bypassing exit-code contract
forgelm/compliance.py medium 4c1bac8 Rollback path in export_compliance_artifacts silently swallows os.remove/os.replace failures
forgelm/compliance.py medium 4c1bac8 Strict HMAC guard rejects only None, lets empty-string secret through at library boundary
forgelm/data_audit/_streaming.py low 5d19ce5 Streaming JSONL reader decodes each line twice in the hot path
forgelm/data.py low 09713ec Falsy non-string System cells bypass clean_string loud-fail gate via truthiness check
forgelm/judge.py medium fbe9ad0 _validate_rubric lets empty {} positional placeholder slip through, crashing .format() with IndexError
forgelm/safety.py medium 82a024d Placeholder-head guard only matches the 2-label LABEL_0/LABEL_1 default, misses LABEL_2+ vocabularies

✅ Already addressed before this pass (15)

These were filed against an earlier commit and the W1–W4 remediation already satisfied them at HEAD:

  • .github/workflows/publish.yml — Obsolete test_cost_estimation.py filename guard in publish.yml (df529b2)
  • docs/usermanuals/en/reference/configuration.md — deployment: contract contradiction in configuration.md (419fc88)
  • docs/usermanuals/tr/compliance/verify-audit.md — Exit-code contract inconsistency (some lines said exit 2 for strict-mode preflight) (419fc88)
  • docs/usermanuals/tr/reference/cli.md — Stale --strict exit-2 reference in TR CLI exit-code table (419fc88)
  • forgelm/_http.py — _PortStrippingAdapter omits server_hostname (SNI) alongside assert_hostname (df529b2)
  • forgelm/cli/_no_train_modes.py — Hardcoded _SANITY_FAILURE_RATE moved into validated SyntheticConfig.sanity_failure_rate (5daf016)
  • forgelm/cli/subcommands/_reverse_pii.py — re.error text echoed into JSON error envelope for --type custom regex (df529b2)
  • forgelm/cli/subcommands/_verify_annex_iv.py — Reject non-dict system_identification in the Annex IV §1 gate (7351e73)
  • forgelm/cli/subcommands/_verify_integrity.py — Validate manifest artifact paths before hashing (type + traversal containment) (df529b2)
  • forgelm/cli/subcommands/_verify_integrity.py — NotADirectoryError already mapped to EXIT_CONFIG_ERROR (exit 1) via dedicated handler (df529b2)
  • forgelm/compliance.py — Per-file compliance bundle promotion can leave a torn bundle on rename failure (7351e73)
  • forgelm/grpo_rewards.py — ANSWER_EXTRACT_PATTERN greedy capture absorbs later same-line Answer: marker (1f96124)
  • forgelm/merging.py — TIES/DARE behavior knobs already exposed as validated MergeConfig fields (5daf016)
  • forgelm/trainer.py — One-shot iterable consumed during gold_answer probe (df529b2)
  • tests/test_check_notebook_pins.py — Python 3.10 tomllib import break at fixture setup (43f206e)

⏭️ Intentionally not changed (3) — documented standards/scope

  • forgelm/compliance.py — FORGELM_ALLOW_AUDIT_REROOT env-var gate on audit-chain re-root: The flagged code is present at HEAD: forgelm/compliance.py:294 if os.getenv("FORGELM_ALLOW_AUDIT_REROOT") != "1": gates the write-time audit-chain re-root, introduced in 70c611f and unchanged at df529b2. CodeRabbit reads this as a forbidden env-var "behaviour toggle." But ForgeLM's own standard scopes the prohibition narrowly:…
  • forgelm/export.py — FORGELM_GGUF_CONVERTER env var is an infra/tool-path discovery fallback for the config-less export…: The finding misapplies the "behavior via validated YAML, not env vars" standard to this case. Two pieces of current evidence: (1) forgelm export has NO YAML/config pipeline. Its dispatcher forgelm/cli/subcommands/_export.py:16-23 calls `export_model(model_path=args.model_path, output_path=args.output, output_format=args.format, quant=args.quant,…
  • tests/test_compliance.py — FORGELM_ALLOW_AUDIT_REROOT env break-glass override should stay env, not move to YAML: The tests exercise a REAL production control: forgelm/compliance.py:294 reads if os.getenv("FORGELM_ALLOW_AUDIT_REROOT") != "1": and raises ConfigError otherwise (the write-time truncation guard introduced in 70c611f, F-P4-OPUS-21). So the finding is not a false positive about the code's existence. However, CodeRabbit's prescribed fix (move it into…

Triage + fixes verified by an automated multi-agent pass; each fix re-confirmed by reverting it to prove the new test fails. Resolving the 27 addressed threads; the 3 skips are left open for maintainer review.

cemililik and others added 4 commits June 15, 2026 16:43
1 critical + 11 high from the multi-agent review (executably verified); each behaviour change ships a regression test. File-grouped, so co-located lower-severity fixes ride along.

F-C-01 (CRITICAL): trainer no longer auto-reverts a good model when safety eval fails for INFRASTRUCTURE reasons (evaluation_completed=False) — keeps the gate + logs instead of destroying the model and writing a false 'safety reverted' audit record.
High: F-H-05 annex_iv set-serialisation hash; F-H-07 config->safety module-topology; F-H-08 email PII regex ReDoS; F-H-09/10 judge/safety malformed-input guards; F-H-11 wizard cancel scope; F-H-01 audit-catalog TR mirror; F-H-02 release.md phase map; F-H-03/04 verify-integrity docs (cli.md+json-output.md EN+TR); F-H-12 notebook pin. Plus hash_file public-rename (F-M-10).

F-H-06 (audit-reroot env->config) DEFERRED to roadmap F-PR29-A6-14: constructor-param forced a public-API MAJOR bump for zero gain (no caller passes it); FORGELM_ALLOW_AUDIT_REROOT break-glass retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 medium: _http redaction + __all__ export surface (F-M-08/F-L-07/21), verify-integrity error envelope + exit-2 test (F-M-09/25), data clean-string (F-M-16), export error_kind (F-M-18/23), merging TIES/DARE (F-M-19/20), JSONL streaming (F-M-17), CHANGELOG + CLAUDE/AGENTS gauntlet mirror (F-M-01/02/03), docs (F-M-04/05/06/07), safety (F-M-21), trainer NaN baseline (F-M-22). Each code change ships a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Low/nit polish across grpo_rewards/_strip_pattern/model_card/inference/synthetic/_safety_eval/_dispatch-parser/_purge + test robustness (grpo warn-once module-state reset, email ReDoS absolute-time guard, data_audit, cli_subcommands, config_doc_parity, gdpr, notebook_pins, eu_ai_act, trainer torch-gate) + docs (roadmap, guides TR, audit). F-L-01 (sample_packing deprecation tracking-issue link) deferred — needs a GitHub issue number.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#61/#63 review)

test_explicit_tier_disagreement_warns flaked in CI (3.11/3.12/3.13): the F-L-11 module-level _tier_disagreement_warned dedup set is populated by an earlier test (test_pipeline_config), suppressing the warning here. Clear the set first, mirroring the resets already in test_pipeline_config — makes the assertion order-independent. Verified by reproducing the polluting order locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik cemililik merged commit 991bbe4 into main Jun 15, 2026
9 of 10 checks passed
cemililik added a commit that referenced this pull request Jun 15, 2026
docs: post-#63 accuracy audit — fix code-vs-doc drift + EN/TR currency
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