Skip to content

feat(phase-14): multi-stage training pipeline chains (SFT → DPO → GRPO) - #53

Merged
cemililik merged 11 commits into
developmentfrom
feat/phase-14-pipeline-chains
May 14, 2026
Merged

cemililik merged 11 commits into
developmentfrom
feat/phase-14-pipeline-chains

Conversation

@cemililik

@cemililik cemililik commented May 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Phase 14 — chain 2+ training stages (SFT → DPO → GRPO) into one config-driven, dry-run-validatable, Annex-IV-traceable run. Implements every Wave 1 task from docs/roadmap/phase-14-pipeline-chains.md.

Backward compatibility: a config without a pipeline: block reaches forgelm/trainer.py byte-identically to v0.6.0 — the orchestrator module is never imported on single-stage runs.

Surface delta

Layer Files Highlights
Config schema forgelm/config.py PipelineStage, PipelineConfig, merge_pipeline_stage_config()
Orchestrator forgelm/cli/_pipeline.py (new) State machine, atomic state file, 5 new audit events, auto-revert + human-approval flow
Compliance forgelm/compliance.py generate_pipeline_manifest(), verify_pipeline_manifest_at_path(), structural+chain integrity helper
CLI forgelm/cli/_parser.py, _dispatch.py, _verify_annex_iv.py --stage, --resume-from, --force-resume, --input-model, verify-annex-iv --pipeline
Webhook forgelm/webhook.py notify_pipeline_started/completed/reverted
Tests tests/test_pipeline_{config,orchestrator,compliance,cli}.py 101 new tests across 4 modules
Fixtures tests/fixtures/pipeline/*.yaml 6 fixtures: minimal_3_stage, inheritance_matrix, gated_pending_approval, auto_revert_at_stage_2, invalid_distributed_per_stage, stale_state_resume_v1+v2
Docs docs/guides/pipeline.md + -tr.md Bilingual operator guide; bilingual parity guard green
Templates config_template.yaml Commented-out pipeline: example block
Changelog CHANGELOG.md::[Unreleased]::Added Detailed entry

Verification

  • ruff format + check: clean (174 files)
  • 11 / 11 self-review gauntlet guards: green (dry-run, bilingual parity, anchor resolution, CLI help consistency, wizard defaults, no-analysis-refs, no-unguarded-sys.modules.pop, site version, format, check)
  • Full pytest tests/ (with --ignore=test_phase12_review_fixes.py, the documented perf-flake from prior reviews): 1879 passed, 13 skipped, 0 failed in 28:30
  • 101 new Phase 14 tests isolated: green in 6.5s

Release-time follow-up (deferred to v0.7.0 cut-release cycle)

Phase 14 doc carries an explicit "Release-time follow-up" callout for docs/usermanuals/, site/features.html, site/index.html, site/js/translations.js, and tools/update_site_version.py. These are release-product surfaces and are intentionally not edited in this PR — they ship under the v0.7.0 tag via cut-release.

Test plan

  • CI cross-OS matrix (Linux / macOS / Windows × Python 3.10-13) green
  • Manual: end-to-end 3-stage SFT → DPO → GRPO run against operator-supplied datasets
  • Manual: forgelm --config pipeline.yaml --dry-run collects every per-stage error
  • Manual: forgelm --stage dpo_stage with missing prev-stage output → EXIT_CONFIG_ERROR=1 with the documented message shape
  • Manual: stale-config-hash resume → rejected; --force-resume allows with WARNING
  • Manual: stage 1 with require_human_approval: true → EXIT_AWAITING_APPROVAL=4, state preserved for resume
  • Manual: forgelm verify-annex-iv --pipeline <run_dir> returns 0 on a clean manifest, non-zero with violations on a tampered one

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Multi-stage training pipelines (SFT → DPO → GRPO chains) with automatic model handoff between stages.
    • New CLI arguments: --stage (run single stage), --resume-from (resume pipeline execution), --force-resume (override config validation), and --input-model (override stage input model).
    • Human approval gates to control stage progression and pause pipeline execution.
    • Pipeline-wide audit logging and webhook notifications for pipeline lifecycle events.
    • Pipeline chain integrity verification via verify-annex-iv --pipeline.
  • Documentation

    • New configuration guides for multi-stage pipeline setup (English and Turkish).
    • Updated configuration template with pipeline examples.
    • Phase 14 implementation status and roadmap updates.

Review Change Stack

Implements every Phase 14 task documented in
``docs/roadmap/phase-14-pipeline-chains.md``.  Single-stage runs reach
``forgelm/trainer.py`` byte-identically to v0.6.0 — the orchestrator
module is only imported when a ``pipeline:`` block is present.

Foundation (Wave 1)

- ``forgelm/config.py``: ``PipelineStage`` + ``PipelineConfig`` Pydantic
  models enforce ``^[a-z0-9_]{1,32}$`` stage names, ≥ 1-stage
  pipelines, unique-name validator, and explicit-``trainer_type``
  audit-clarity rule per stage.  ``extra="forbid"`` rejects per-stage
  ``distributed`` / ``webhook`` / ``compliance`` / ``risk_assessment``
  / ``monitoring`` / ``retention`` / ``synthetic`` / ``merge`` /
  ``auth`` with the offending key in the Pydantic error.
- ``merge_pipeline_stage_config()`` helper materialises a flat
  ``ForgeConfig`` per stage with section-wholesale inheritance + the
  4-priority auto-chain resolution rule (``--input-model`` > explicit
  stage.model > prev.output_dir/final_model > root.model.name_or_path).
- ``ForgeConfig.pipeline: Optional[PipelineConfig]`` + ``model_rebuild()``
  to resolve the forward reference.

Orchestrator (Wave 2)

- ``forgelm/cli/_pipeline.py`` (new module): ``PipelineOrchestrator``
  drives the chain.  ``PipelineStageState`` + ``PipelineState``
  dataclasses round-trip to ``pipeline_state.json`` via atomic write
  (tmp + ``os.replace``).  ``pipeline.started`` /
  ``pipeline.stage_started`` / ``pipeline.stage_completed`` /
  ``pipeline.stage_reverted`` / ``pipeline.completed`` audit events
  fire alongside the existing per-stage ``training.*`` vocabulary so
  pre-existing dashboards keep working.
- ``--stage <name>`` partial-run requires the previous stage's
  on-disk output (auto-resolved) or ``--input-model <path>``
  (escape hatch); silent fallback to root model is hard-failed.
- ``--resume-from <name>`` skips ``completed`` stages whose
  output_model path exists.  Stale-state guard refuses to resume
  when ``pipeline_config_hash`` differs from the current YAML
  bytes; ``--force-resume`` bypasses with a WARNING + audit record.
- Human-approval gate inside a pipeline exits
  ``EXIT_AWAITING_APPROVAL (4)`` and leaves downstream stages
  ``pending`` (not ``skipped_due_to_prior_revert``) so resume
  picks them up post-approval.

Compliance (Wave 3)

- ``forgelm/compliance.py``: ``generate_pipeline_manifest()`` builds
  the Annex IV chain index documented in Phase 14 Task 3 schema.
  ``export_pipeline_manifest()`` writes it atomically.
  ``verify_pipeline_manifest()`` walks a pipeline run directory and
  returns a ``VerifyPipelineResult`` (chain integrity + per-stage
  manifest existence).  ``_verify_manifest_payload()`` is the
  in-memory helper for unit tests.
- ``forgelm verify-annex-iv --pipeline <run_dir>`` CLI mode validates
  a complete chain and exits non-zero on any violation.

CLI + webhook (Wave 4)

- ``forgelm/cli/_parser.py``: new ``--stage``, ``--resume-from``,
  ``--force-resume``, ``--input-model`` top-level flags.
- ``forgelm/cli/_dispatch.py``: routes to the orchestrator when
  ``config.pipeline`` is set; passes the raw YAML bytes for the
  config-hash guard.
- ``forgelm/webhook.py``: ``notify_pipeline_started`` /
  ``notify_pipeline_completed`` / ``notify_pipeline_reverted``
  methods; gated by the existing ``notify_on_start`` /
  ``notify_on_success`` / ``notify_on_failure`` config flags.

Tests + fixtures (Waves 2-5)

- ``tests/test_pipeline_config.py`` — 53 schema + inheritance-matrix
  + auto-chain priority tests.
- ``tests/test_pipeline_orchestrator.py`` — 27 state-machine tests
  covering happy-path / auto-revert / human-approval / resume /
  stale-config / partial-filter / webhook-failure-tolerance.
- ``tests/test_pipeline_compliance.py`` — 14 manifest schema +
  chain-integrity verifier tests (including partial-filter and
  auto-revert manifest acceptance).
- ``tests/test_pipeline_cli.py`` — 7 flag-interaction +
  dispatch-routing tests.
- 6 fixture YAMLs under ``tests/fixtures/pipeline/`` exercising
  every documented scenario.

Operator surface (Waves 6-7)

- ``docs/guides/pipeline.md`` + ``docs/guides/pipeline-tr.md`` —
  bilingual operator guide (when to use, inheritance matrix, CLI
  semantics, human-approval flow, Annex IV verifier, Limitations).
- ``config_template.yaml`` carries a commented-out ``pipeline:``
  example block.
- ``CHANGELOG.md`` ``[Unreleased]`` section ``Added (Phase 14 — …)``
  + ``Changed (Phase 14)`` entries.
- ``docs/roadmap/phase-14-pipeline-chains.md`` Status header
  updated to "Implementation complete on development; awaiting
  v0.7.0 PyPI tag".
- ``tools/check_bilingual_parity.py`` ``_PAIRS`` registers the new
  guide pair.

Wizard / notebooks / CLI help

- ``forgelm wizard`` is **not** extended with a pipeline path
  (operator-grade workflow; documented in the guide's Limitations).
- ``notebooks/`` carries no new pipeline notebook (fixture suite
  serves the same purpose without 3× boilerplate duplication).
- 451 ``forgelm`` invocations across docs / README pass the
  CLI-help-consistency guard against the live parser surface.

Verification

- ruff format + check: clean across 174 files.
- 11 / 11 self-review gauntlet guards green (dry-run, bilingual
  parity, anchor resolution, CLI help consistency 451
  invocations, wizard defaults, no-analysis-refs, no-unguarded-
  sys.modules-pop, site-version-literal sync).
- 1879 passed / 13 skipped / 0 failed across the full pytest
  suite (28 min 35 sec; ``tests/test_phase12_review_fixes.py``
  perf-flake remains documented out-of-scope).
- All 4 new Phase 14 test modules: 101 / 101 pass in 11 sec.

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

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fcd12b9-f17b-474b-b732-e6e861905fd9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-14-pipeline-chains

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

❤️ Share

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

@codacy-production

codacy-production Bot commented May 14, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 433 complexity · 22 duplication

Metric Results
Complexity 433
Duplication 22

View in Codacy

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces multi-stage training pipeline chains, allowing sequential execution of stages like SFT, DPO, and GRPO via a new orchestrator. It includes updated configuration models, CLI support for stage resumption, and Annex IV manifest generation for compliance. Review feedback identified the need to prevent output directory collisions during dry runs, harden state file deserialization against corruption, and fix a path validation bug for auto-chained models.

Comment thread forgelm/cli/_pipeline.py
Comment on lines +539 to +561
def dry_run(self) -> int:
"""Validate every stage without allocating any GPU resources.

Collects all per-stage Pydantic / merge errors before exiting,
mirroring ``pytest --collectonly``. Returns
:data:`EXIT_CONFIG_ERROR (1)` if any stage fails validation,
:data:`EXIT_SUCCESS (0)` otherwise.
"""
errors: List[str] = []
prev_output: Optional[str] = None
for stage in self.pipeline.stages:
try:
merged = merge_pipeline_stage_config(
self.root_cfg,
stage,
prev_output_model=prev_output,
)
except Exception as exc: # noqa: BLE001 — best-effort: collect every per-stage validation error for a single operator report instead of stopping at the first.
errors.append(f"Stage {stage.name!r}: merge failed: {exc}")
continue

# Project the output path the next stage would auto-chain to.
prev_output = os.path.join(merged.training.output_dir, "final_model")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The dry_run method should validate that each stage has a unique training.output_dir. Since stages inherit the root config's output_dir by default, a multi-stage pipeline where stages omit this field will cause per-stage manifests and checkpoints to overwrite each other, breaking the Annex IV chain of custody.

    def dry_run(self) -> int: 
        """Validate every stage without allocating any GPU resources.

        Collects all per-stage Pydantic / merge errors before exiting,
        mirroring ``pytest --collectonly``.  Returns
        :data:`EXIT_CONFIG_ERROR (1)` if any stage fails validation,
        :data:`EXIT_SUCCESS (0)` otherwise.
        """
        errors: List[str] = []
        prev_output: Optional[str] = None
        seen_dirs: set[str] = set()
        for stage in self.pipeline.stages:
            try:
                merged = merge_pipeline_stage_config(
                    self.root_cfg,
                    stage,
                    prev_output_model=prev_output,
                )

                # Check for output_dir collisions to prevent manifest/checkpoint overwrites.
                out_dir = os.path.abspath(merged.training.output_dir)
                if out_dir in seen_dirs:
                    errors.append(f"Stage {stage.name!r}: output_dir collision: {merged.training.output_dir}")
                seen_dirs.add(out_dir)
            except Exception as exc:  # noqa: BLE001 — best-effort: collect every per-stage validation error for a single operator report instead of stopping at the first.
                errors.append(f"Stage {stage.name!r}: merge failed: {exc}")
                continue

            # Project the output path the next stage would auto-chain to.
            prev_output = os.path.join(merged.training.output_dir, "final_model")

Comment thread forgelm/cli/_pipeline.py Outdated
Comment on lines +628 to +638
def _load_state_file(self) -> Optional[PipelineState]:
path = self.paths["state_file"]
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
payload = json.load(f)
except (OSError, json.JSONDecodeError) as e:
logger.warning("Failed to read pipeline_state.json at %s: %s. Treating as missing.", path, e)
return None
return _deserialise_state(payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The _load_state_file method should catch potential TypeError, KeyError, or AttributeError that might be raised during _deserialise_state if the state file is corrupted or tampered with. This ensures the orchestrator handles malformed state files gracefully by treating them as missing.

Suggested change
def _load_state_file(self) -> Optional[PipelineState]:
path = self.paths["state_file"]
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
payload = json.load(f)
except (OSError, json.JSONDecodeError) as e:
logger.warning("Failed to read pipeline_state.json at %s: %s. Treating as missing.", path, e)
return None
return _deserialise_state(payload)
def _load_state_file(self) -> Optional[PipelineState]:
path = self.paths["state_file"]
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
payload = json.load(f)
return _deserialise_state(payload)
except (OSError, json.JSONDecodeError, AttributeError, TypeError, KeyError) as e:
logger.warning("Failed to read pipeline_state.json at %s: %s. Treating as missing.", path, e)
return None

Comment thread forgelm/cli/_pipeline.py Outdated
Comment on lines +755 to +763
expected_dir = os.path.dirname(stage_cfg.model.name_or_path)
if not os.path.isdir(expected_dir):
stage_state.error = (
f"Stage {stage.name!r} requires the previous stage's output at "
f"{stage_cfg.model.name_or_path}; pass --input-model <path> to "
f"override or run the full pipeline first."
)
logger.error(stage_state.error)
return EXIT_CONFIG_ERROR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The existence check for auto-chained models uses os.path.dirname on the model path, which checks the parent directory instead of the model directory itself. If the parent directory exists but the final_model directory is missing (e.g., due to a previous stage crash), the check will pass, leading to a failure later in the trainer. The check should be performed on the full path directly.

Suggested change
expected_dir = os.path.dirname(stage_cfg.model.name_or_path)
if not os.path.isdir(expected_dir):
stage_state.error = (
f"Stage {stage.name!r} requires the previous stage's output at "
f"{stage_cfg.model.name_or_path}; pass --input-model <path> to "
f"override or run the full pipeline first."
)
logger.error(stage_state.error)
return EXIT_CONFIG_ERROR
if not os.path.isdir(stage_cfg.model.name_or_path):
stage_state.error = (
f"Stage {stage.name!r} requires the previous stage's output at "
f"{stage_cfg.model.name_or_path}; pass --input-model <path> to "
f"override or run the full pipeline first."
)
logger.error(stage_state.error)
return EXIT_CONFIG_ERROR

Addresses two review cycles on the Phase 14 pipeline-chains branch.

Blocking (Review 1):
- B-1 dispatch order: pipeline branch now runs before _maybe_run_no_train_mode
  so --dry-run reaches the orchestrator instead of the single-stage validator.
- B-2 strong-reference revert: stale state-file paths from prior runs no
  longer leak into the resume flow; hash mismatch now hard-aborts with a
  precise error unless --force-resume is given.
- B-3 chain integrity: _verify_manifest_payload now compares each stage
  against its immediate predecessor rather than the most-recent-with-output,
  so a hash mismatch on stage N cannot be masked by stage N-2's output.

Significant:
- F-1 leaf-vs-parent dir: replaced os.path.dirname(...) with
  os.path.isdir(stage_cfg.model.name_or_path) — the parent check was
  always true for any non-root path.
- F-2 empty --input-model: '' is normalised to None at dispatch time so
  the orchestrator never sees a falsy override that the truthy guard
  would silently treat as 'no override'.
- F-3 --force-resume audit trail: force path now emits a dedicated
  pipeline.force_resume event with both old + new pipeline_config_hash
  before mutating state. Mismatch path without --force returns a
  precise diagnostic string rather than raising.
- S-2 finalised-manifest verifier: flags a manifest that still has
  running stages as a structural integrity failure.
- S-4 atomic state writes: pipeline_state.json writes go through a
  tmp + os.replace round-trip to survive crashes mid-write.
- R1-S4 tamper guard: _load_state_file catches KeyError/TypeError/
  ValueError from _deserialise_state and treats a structurally invalid
  state file as 'no resume state' instead of crashing.

Nitpicks:
- N-1 docstring drift on PipelineStage.training (clarified that
  trainer_type is required when training: is supplied).
- N-2 verifier flags running stages on finalised manifest.
- N-3 audit-write failures are non-fatal — pipeline continues, warning
  is logged, regression test added.
- N-5 _emit_summary JSON is round-trippable (regression test added).
- N-6 gated stages emit a dedicated pipeline.stage_gated audit event
  (separate from .stage_completed).
- N-7 inheritance-matrix 'data' row marked 'strongly discouraged' in
  both EN + TR guides and the roadmap doc.
- N-9 release-time follow-up callout added to roadmap doc.
- N-10 force-resume test now asserts both old + new hashes in the audit
  payload, not just one.

N-8 (test helper deduplication into tests/_helpers/) deferred to a
follow-up PR — review explicitly labeled it 'Skip — follow-up PR'.

Verification:
- pytest tests/test_pipeline_*.py → 115 passed
- ruff format + ruff check → clean
- 11-guard gauntlet → all green (dry-run, bilingual parity, anchors,
  wizard defaults, no-analysis-refs, sys.modules guard, site version,
  CLI-help consistency, field descriptions, pip-audit, bandit)

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

Copy link
Copy Markdown
Collaborator Author

Review-response — Phase 14 pipeline chains

Combined response to both review cycles (Review 1: 3 blocking + 3 significant + 1 nitpick; Review 2: 3 significant + 10 nitpicks). All findings addressed in 0d58f1b on top of a01308c.

Blocking (Review 1) — all fixed

  • B-1 dispatch order — forgelm/cli/_dispatch.py: pipeline branch now executes before _maybe_run_no_train_mode. --dry-run previously silently routed to the single-stage validator. Regression test: TestTopLevelDispatchOrdering in tests/test_pipeline_cli.py.
  • B-2 stale state-file revert — forgelm/cli/_pipeline.py: hash mismatch hard-aborts the resume path with a precise error; --force-resume is the only escape hatch and now leaves a tamper-evident audit trail.
  • B-3 weak chain integrity — forgelm/compliance.py::_verify_manifest_payload: now compares each stage against its immediate predecessor rather than the most-recent-stage-with-output, so a swapped intermediate adapter can no longer be masked. Tests: TestStrictChainIntegrity (4 cases) in tests/test_pipeline_compliance.py.

Significant — all fixed

  • F-1 leaf-vs-parent dir check — replaced os.path.dirname(stage_cfg.model.name_or_path) with direct os.path.isdir(stage_cfg.model.name_or_path). The parent-dir check was always-true for any non-root path.
  • F-2 empty --input-model "" — normalised to None at dispatch in run_pipeline_from_args (raw_input_model if raw_input_model else None). Regression test: test_empty_input_model_normalised_to_none.
  • F-3 --force-resume audit trail — force path emits pipeline.force_resume with both old_config_hash + new_config_hash before mutating state; non-force mismatch path returns a precise diagnostic string instead of raising. Test: test_force_resume_emits_audit_event_with_both_hashes.
  • S-2 finalised-manifest verifier — flags a finalised manifest that still contains running stages as a structural failure. Test: TestVerifierFlagsRunningOnFinalisedManifest.
  • S-4 atomic state writes — _save_state writes to a .tmp sibling and os.replaces, surviving mid-write crashes.
  • R1-S4 tamper guard — _load_state_file catches KeyError | TypeError | ValueError from _deserialise_state, logs a warning, and treats a structurally invalid pipeline_state.json as "no resume state" rather than crashing.

Nitpicks (Review 2) — addressed

  • N-1 docstring drift on PipelineStage.training — clarified trainer_type requirement.
  • N-2 verifier flags running stages on finalised manifest (overlaps S-2; same fix).
  • N-3 audit-write failures are non-fatal — warning logged, pipeline continues. Test: test_audit_log_write_failure_does_not_abort_pipeline.
  • N-5 _emit_summary JSON output is round-trippable. Test: test_emit_summary_json_output_is_round_trippable.
  • N-6 gated stages emit a dedicated pipeline.stage_gated audit event (no longer overloaded onto .stage_completed). Test: test_gated_stage_emits_dedicated_stage_gated_audit_event.
  • N-7 inheritance-matrix "data" row updated to strongly discouraged in docs/guides/pipeline.md, docs/guides/pipeline-tr.md, and docs/roadmap/phase-14-pipeline-chains.md.
  • N-9 release-time follow-up callout added to roadmap doc.
  • N-10 force-resume test asserts both old + new hashes in the audit payload.

Deferred

  • N-8 test-helper deduplication into tests/_helpers/ — review explicitly labeled "Skip — follow-up PR". Will land as a separate refactor PR after Phase 14 is merged.

Verification

pytest tests/test_pipeline_*.py     → 115 passed in 8.15s
ruff format .                       → 4 files reformatted (clean)
ruff check --fix .                  → 3 auto-fixed, 0 remaining
11-guard gauntlet                   → all green
  ✓ dry-run
  ✓ 47 bilingual doc pair(s)
  ✓ 272 markdown anchors resolved
  ✓ shipped wizard defaults
  ✓ no public-tree analysis refs
  ✓ 174 Python files / sys.modules guard
  ✓ site/ version literals = v0.6.0
  ✓ 451 forgelm CLI invocations consistent
  ✓ field descriptions
  ✓ pip-audit / bandit

Ready for re-review.

Three new findings from gemini-code-assist on top of the 0d58f1b batch
+ a small hardening tweak. All three appear in the PR as inline review
comments on forgelm/cli/_pipeline.py.

F-G-1 — output_dir collision check in dry-run (medium):
  Two stages resolving to the same training.output_dir silently overwrite
  each other's per-stage manifests + checkpoints. The footgun is real
  for stages that inherit the root training block wholesale. dry_run()
  now flags the collision (both explicit-override and inherited cases)
  via EXIT_CONFIG_ERROR before any GPU work.

F-G-2 — AttributeError on tampered state (medium):
  Round out the _load_state_file shape-tamper guard with AttributeError
  alongside KeyError / TypeError / ValueError. Covers the case where a
  hand-crafted stages: [null, "foo"] state file makes s.items() raise
  AttributeError on the non-dict item.

F-G-3 — chain-existence leaf check:
  Already addressed in 0d58f1b (F-1); re-confirmed here.

Tests added:
- test_dry_run_flags_output_dir_collision — explicit collision via two
  stage training blocks with the same output_dir.
- test_dry_run_flags_inherited_output_dir_collision — two stages that
  both inherit the root training.output_dir (no per-stage override).

CHANGELOG: extended the review-response section with the F-N-1
pipeline.stage_gated event (also new in this round) and the F-G-1
collision guard.

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

Copy link
Copy Markdown
Collaborator Author

Phase 14 PR #53 — Gemini-inline review response (commit b8512f0)

Addressed all 3 inline comments from gemini-code-assist[bot]:

1. forgelm/cli/_pipeline.py:561 — dry_run output_dir collision check ✅
dry_run() now tracks abspath(merged.training.output_dir) across stages and emits a per-stage error if any two collide. Covers both the explicit-override and inherited-from-root cases. Tests: test_dry_run_flags_output_dir_collision + test_dry_run_flags_inherited_output_dir_collision.

2. forgelm/cli/_pipeline.py:638 — _load_state_file shape catch ✅
AttributeError added to the except (KeyError, TypeError, ValueError, AttributeError) tuple alongside the existing R1-S4 guard from commit 0d58f1b. Comment explains the stages: [null, "foo"] case where s.items() raises AttributeError.

3. forgelm/cli/_pipeline.py:763 — chain leaf-dir check ✅
Already addressed in commit 0d58f1b (F-1): the guard now uses os.path.isdir(stage_cfg.model.name_or_path) directly, no longer os.path.dirname(...).

Verification

  • ruff format + ruff check clean.
  • 117/117 pipeline tests pass; 4 new tests cover the Gemini findings.
  • Full pytest suite: 1924 passed / 14 skipped (2 perf-timing flakes in test_phase12_review_fixes.py::TestRegexLinearity due to concurrent CPU contention; both pass in isolation).
  • All 11 self-review guards green.

Addresses the Codacy "2 new issues" gate and the SonarCloud "Quality Gate
failed (E Reliability Rating on New Code)" gate on the Phase 14 PR.

BLOCKER bugs (SonarCloud python:S930 — root cause of the Reliability
Rating regression):
- forgelm/webhook.py _send: pipeline notifiers (notify_pipeline_started/
  completed/reverted) were passing stage_count / final_status /
  stopped_at / stage_name as keyword arguments to a fixed-signature
  _send method.  TypeError raised at runtime, swallowed by the pipeline
  orchestrator's best-effort try/except → pipeline webhooks silently
  never fired in production.
  _send now accepts **extra and merges them into the payload (with a
  base-field-collision guard so the contract stays stable).

MAJOR bugs:
- tests/test_pipeline_compliance.py _three_stage_state: removed dead
  `"completed" if all_completed else "completed"` ternary that always
  returned the same value (SonarCloud python:S3923).  Dropped the unused
  `all_completed` kwarg too.
- tests/test_pipeline_config.py: switched a float == 1.5 assertion to
  pytest.approx() (SonarCloud python:S1244).

MAJOR code smells:
- forgelm/cli/_pipeline.py _run_single_stage merge-failure branch:
  swapped logger.error(... exc) for logger.exception(...) (Sonar S8572)
  so the traceback lands in the log alongside the error message.
- Trainer-crash catch: pulled the long inline noqa rationale out into a
  preceding comment block so the # noqa: BLE001 marker has nothing
  trailing it (Sonar python:S7632 disliked the trailing — em-dash
  prose).

Codacy + minor smells:
- tests/test_pipeline_orchestrator.py: round = _deserialise_state(...)
  shadowed the builtin; renamed to round_tripped.
- tests/test_pipeline_compliance.py: renamed test_*is_OK to test_*is_ok
  so the method name matches the standard snake_case regex.
- tests/test_pipeline_cli.py _ns: dict(...) constructor → {} literal.

Verification:
- ruff format + ruff check clean.
- 149/149 pipeline + webhook tests pass.
- All 11 self-review guards green.

Cognitive-complexity CRITICAL code smells (run() at 53, _verify_*
at 24, etc.) are deliberately not addressed in this commit — they are
Maintainability-rating items and do not affect the failing
Reliability Rating gate, and refactoring them safely without
behavioural drift would warrant its own PR.

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

Copy link
Copy Markdown
Collaborator Author

CI Quality Gate response — commit b3c6a45

Codacy (2 issues) + SonarCloud (16 issues, E Reliability Rating) gates addressed:

BLOCKER bugs fixed (SonarCloud python:S930 — root cause of the Reliability regression)

  • 4× forgelm/webhook.py _send unknown-kwarg violations (stage_count / final_status / stopped_at / stage_name). notify_pipeline_* methods were passing kwargs the fixed _send signature didn't accept; the TypeError was being swallowed by the orchestrator's best-effort try/except, so pipeline webhooks silently never fired. _send now accepts **extra and merges into the payload.

MAJOR bugs fixed

  • tests/test_pipeline_compliance.py — dead "completed" if all_completed else "completed" ternary (S3923) removed; unused kwarg dropped.
  • tests/test_pipeline_config.py — float == 1.5 → pytest.approx(1.5) (S1244).

MAJOR code smells fixed

  • _run_single_stage merge-failure branch: logger.error(..., exc) → logger.exception(...) (S8572).
  • Trainer-crash catch: pulled the long inline # noqa: BLE001 — … rationale out to a preceding comment block (S7632).

Codacy + minor smells

  • tests/test_pipeline_orchestrator.py:184 — round = … shadowed builtin → round_tripped.
  • tests/test_pipeline_compliance.py:385 — test_*is_OK → test_*is_ok (snake_case).
  • tests/test_pipeline_cli.py:57 — dict(...) → {} literal.

Out of scope

CRITICAL cognitive-complexity smells (run() at 53, _verify_manifest_payload at 24, etc.) are Maintainability-rating items, not Reliability bugs — they don't block the failing gate. Refactoring run() from 53 to 15 without behavioural drift warrants its own PR.

Verification

  • ruff format + check clean
  • 149/149 pipeline + webhook tests pass
  • All 11 self-review guards green

Addresses the 5 remaining CRITICAL code-smell findings on the Phase 14
PR by extracting helper methods.  Behaviour is preserved end-to-end —
the 227-test sweep (pipeline + CLI + verify-annex-iv + webhook +
library API) passes byte-identical to b3c6a45.

forgelm/cli/_dispatch.py:166 _main_inner (17 → ≤15):
  Extracted _dispatch_legacy_data_audit (the --data-audit alias path
  with its DeprecationWarning + audit-log breadcrumb + delegation to
  _run_data_audit) and _dispatch_pipeline_mode (the YAML re-read +
  run_pipeline_from_args dispatch).  _main_inner is now a thin
  argparse-then-route entry point.

forgelm/cli/_pipeline.py:289 run() (53 → ≤15):
  This was the biggest one.  Split into six helpers, each with one
  cohesive responsibility:
  - _prepare_resume_or_init_state — loads existing state for resume
    or builds a fresh one.  Returns (state, None) on success or
    (None, EXIT_CONFIG_ERROR) for invalid resume paths.
  - _resolve_resume_skiplist — walks state.stages before the resume
    point and builds the "already-completed on disk" skip list.
  - _resolve_stage_filter_setup — validates --stage <name> + seeds
    the auto-chain prev_output when filtering to a non-first stage.
  - _execute_one_stage — per-stage setup (started_at / status /
    trainer_type / save) + delegates to _run_single_stage + records
    duration.
  - _handle_stage_outcome — translates the per-stage exit code into
    the next-state transition (gated / completed / failed) +
    audit/webhook event + chain_broken flag.
  - _finalise_pipeline — phase 5+6: terminal state + pipeline.completed.

  The retained loop is just dispatch + skip/done bookkeeping; the
  branching that drove the cognitive-complexity score lives in
  the helpers, each at single-digit complexity.

forgelm/cli/_pipeline.py:821 _run_single_stage (18 → ≤15):
  Extracted:
  - _resolve_input_source — pure function returning the
    InputSourceLiteral via the documented priority ladder (CLI
    override > stage explicit > chain > root).
  - _merge_and_validate_stage — merge_pipeline_stage_config wrap +
    chain-presence guard.  Returns the merged ForgeConfig or None
    on a config-error path (caller maps to EXIT_CONFIG_ERROR).
  - _invoke_trainer — heavy-import-guarded ForgeTrainer lifecycle.
    Returns the TrainResult or None on a runtime/import failure.

  _run_single_stage is now linear: resolve source → merge → audit
  start → invoke trainer → record results.

forgelm/cli/subcommands/_verify_annex_iv.py:188 (27 → ≤15):
  Extracted:
  - _run_pipeline_mode — --pipeline flag branch (manifest + chain-
    integrity violations).
  - _verify_artefact_and_handle_io_errors — the FileNotFoundError /
    IsADirectoryError / JSONDecodeError / OSError → exit-code
    mapping that handles the single-artefact path's I/O failures.
  - _print_artefact_result — text + JSON output formatting.

  _run_verify_annex_iv_cmd is now: validate path → branch on mode →
  delegate.

forgelm/compliance.py:1057 _verify_manifest_payload (24 → ≤15):
  Lifted the per-stage chain-integrity check into _check_chain_link
  (pure function, idx-based for "compare against immediate
  predecessor") and the stopped_at + running-stage finalised checks
  into _check_status_consistency.  Top-level keys list lifted to
  module-level constant _PIPELINE_MANIFEST_REQUIRED_KEYS.

All 11 self-review guards remain green; ruff format + check clean;
227 / 227 tests pass on the broader pipeline + CLI sweep.

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

Copy link
Copy Markdown
Collaborator Author

SonarCloud cognitive complexity — commit a65d3c9

All 5 CRITICAL python:S3776 findings addressed by extracting helper methods. Behaviour preserved end-to-end (227/227 pipeline + CLI + verify-annex-iv + webhook + library API tests pass byte-identical).

File Function Before Strategy
forgelm/cli/_dispatch.py:166 _main_inner 17 → ≤15 Extracted _dispatch_legacy_data_audit + _dispatch_pipeline_mode
forgelm/cli/_pipeline.py:289 run() 53 → ≤15 Split into 6 helpers: _prepare_resume_or_init_state, _resolve_resume_skiplist, _resolve_stage_filter_setup, _execute_one_stage, _handle_stage_outcome, _finalise_pipeline
forgelm/cli/_pipeline.py:821 _run_single_stage 18 → ≤15 Extracted _resolve_input_source (pure), _merge_and_validate_stage, _invoke_trainer
forgelm/cli/subcommands/_verify_annex_iv.py:188 _run_verify_annex_iv_cmd 27 → ≤15 Extracted _run_pipeline_mode, _verify_artefact_and_handle_io_errors, _print_artefact_result
forgelm/compliance.py:1057 _verify_manifest_payload 24 → ≤15 Lifted check groups into _check_chain_link (pure) + _check_status_consistency; required-keys → module constant

Verification:

  • ruff format + check clean
  • 227 / 227 pipeline + CLI + verify-annex-iv + webhook + library API tests pass
  • All 11 self-review guards green

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with 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.

Inline comments:
In `@CHANGELOG.md`:
- Around line 11-16: The changelog currently contradicts itself about required
stage count; pick the true contract (e.g., minimum 2 stages) and make the
wording consistent across the bullet for
PipelineConfig/PipelineStage/ForgeConfig: replace both “2+ stages” and “≥1-stage
minimum” with a single clear phrase such as “minimum of 2 stages” (or “at least
1 stage” if that is the intended rule), and ensure the text references the same
constraint enforced by PipelineConfig and PipelineStage validation.

In `@docs/guides/pipeline-tr.md`:
- Line 3: Update the phrasing that currently states "Faz 14 — `v0.7.0` ile
geldi." to a pre-release form indicating planned or targeted status (e.g., "Faz
14 — `v0.7.0` için eklendi/hedefleniyor" or equivalent) so it does not read as
already shipped; replace the exact string "Faz 14 — `v0.7.0` ile geldi." with
the chosen pre-release wording.

In `@docs/guides/pipeline.md`:
- Line 3: Update the release-tense wording in the document where the phrase
"Phase 14 — added in `v0.7.0`." appears: replace "added in `v0.7.0`" with a
non-final wording such as "targeted for `v0.7.0`" (or "planned for `v0.7.0`") so
the line reads e.g. "Phase 14 — targeted for `v0.7.0`."

In `@forgelm/cli/_pipeline.py`:
- Around line 747-803: The method _validate_resume_state currently accepts a
stale config when force is true but doesn't verify stage topology; add a
pre-accept guard that compares existing.stages to the current
self.pipeline.stages (e.g., count and stable identifiers like stage names/order)
before emitting the warning/audit and updating existing.pipeline_config_hash; if
they differ, log and return an error string (don’t update the stored hash or
call _audit_event), otherwise proceed with the existing warning, call to
self._audit_event("pipeline.force_resume", ...), and update
existing.pipeline_config_hash as now implemented.

In `@forgelm/cli/subcommands/_verify_annex_iv.py`:
- Around line 188-218: The code in _run_pipeline_mode currently treats any
violations from verify_pipeline_manifest_at_path as EXIT_CONFIG_ERROR; instead,
catch runtime I/O errors separately and map them to EXIT_TRAINING_ERROR. Wrap
the call to verify_pipeline_manifest_at_path(path) in a try/except that catches
OSError/FileNotFoundError/PermissionError (or the specific exception
verify_pipeline_manifest_at_path raises for unreadable manifests) and on those
exceptions print a descriptive FAIL message and sys.exit(EXIT_TRAINING_ERROR);
keep the existing handling for returned violations (non-I/O validation errors)
using EXIT_CONFIG_ERROR. Also, if verify_pipeline_manifest_at_path encodes
unreadable-manifest as a special violation string, detect that string in
violations and treat it as EXIT_TRAINING_ERROR instead of EXIT_CONFIG_ERROR.

In `@forgelm/compliance.py`:
- Around line 1792-1802: The loop assumes manifest.get("stages") is a list of
dicts and uses s.get(...), which raises on malformed payloads; update the code
around _verify_manifest_payload(manifest) and the loop to first validate that
stages = manifest.get("stages") is a list, appending a violation if it is not,
and inside the for loop ensure each item s is a dict before calling s.get
(append a violation like "stage at index X is not an object" when not), then
only check per_stage_manifest and os.path.isfile for dict stages so violations
is returned instead of raising.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdab5bfd-a8e1-4954-94d6-dbca3edc315b

📥 Commits

Reviewing files that changed from the base of the PR and between c8e9446 and a65d3c9.

📒 Files selected for processing (24)
  • CHANGELOG.md
  • config_template.yaml
  • docs/guides/pipeline-tr.md
  • docs/guides/pipeline.md
  • docs/roadmap/phase-14-pipeline-chains.md
  • forgelm/cli/_dispatch.py
  • forgelm/cli/_parser.py
  • forgelm/cli/_pipeline.py
  • forgelm/cli/subcommands/_verify_annex_iv.py
  • forgelm/compliance.py
  • forgelm/config.py
  • forgelm/webhook.py
  • tests/fixtures/pipeline/auto_revert_at_stage_2.yaml
  • tests/fixtures/pipeline/gated_pending_approval.yaml
  • tests/fixtures/pipeline/inheritance_matrix.yaml
  • tests/fixtures/pipeline/invalid_distributed_per_stage.yaml
  • tests/fixtures/pipeline/minimal_3_stage.yaml
  • tests/fixtures/pipeline/stale_state_resume_v1.yaml
  • tests/fixtures/pipeline/stale_state_resume_v2.yaml
  • tests/test_pipeline_cli.py
  • tests/test_pipeline_compliance.py
  • tests/test_pipeline_config.py
  • tests/test_pipeline_orchestrator.py
  • tools/check_bilingual_parity.py

Comment thread CHANGELOG.md Outdated
Comment thread docs/guides/pipeline-tr.md Outdated
Comment thread docs/guides/pipeline.md Outdated
Comment thread forgelm/cli/_pipeline.py
Comment thread forgelm/cli/subcommands/_verify_annex_iv.py
Comment thread forgelm/compliance.py
cemililik and others added 5 commits May 14, 2026 14:39
Each finding was first verified against current code; fixes are
minimal and validated.  All 232 broader-sweep tests pass; 11/11
self-review guards green; ruff format + check clean.

1) CHANGELOG.md — contradictory stage-count wording (valid):
   "2+ training stages" + "≥1-stage minimum" said two different
   things.  Code enforces ``min_length=1``.  Unified the bullet to
   "one or more training stages" + "at least 1 stage (`min_length=1`
   — …)" with the schema's nuance preserved (single-stage runs are
   better served by omitting the `pipeline:` block, but the schema
   accepts a 1-element pipeline for incremental authoring).

2) docs/guides/pipeline.md — release-tense wording (valid):
   v0.7.0 is still ``[Unreleased]`` in CHANGELOG; the guide read
   as already-shipped.  "added in `v0.7.0`" → "targeted for `v0.7.0`
   (currently on the `Unreleased` line of the CHANGELOG)".

3) docs/guides/pipeline-tr.md — release-tense wording (valid):
   Same fix in Turkish.  "`v0.7.0` ile geldi" → "`v0.7.0` için
   hedefleniyor (şu an CHANGELOG'un `Unreleased` satırında)".

4) forgelm/cli/_pipeline.py:_validate_resume_state — topology
   guard for `--force-resume` (valid):
   Even under --force-resume the old code accepted any stale state,
   so a renamed/inserted/deleted stage between runs would make
   ``state.stages[i]`` address a different stage than
   ``self.pipeline.stages[i]`` while the orchestrator iterates
   positionally.  Added a pre-emit guard that compares the ordered
   stage-name list (count + identifier + order); on mismatch we log
   + return an error string *before* the audit hook fires and
   before the stored hash is overwritten, so the JSONL audit trail
   doesn't carry a force_resume event for a refused resume.
   Regression test: test_force_resume_refuses_when_stage_topology_changed.

5) forgelm/cli/subcommands/_verify_annex_iv.py:_run_pipeline_mode —
   I/O-vs-config exit-code mapping (valid):
   ``verify_pipeline_manifest_at_path`` already encodes I/O failures
   as ``"… unreadable: …"`` sentinel strings, but the CLI mapped
   every non-empty violation list to ``EXIT_CONFIG_ERROR (1)``,
   contradicting the single-artefact path's policy
   (FileNotFoundError → 1, OSError → 2 via
   _verify_artefact_and_handle_io_errors).  Now:
   - try/except wraps the verifier call (defence-in-depth against
     a future change that lets exceptions bubble) and maps OSError
     → ``EXIT_TRAINING_ERROR (2)``.
   - The "unreadable" sentinel in the returned violations also maps
     to exit 2; everything else (including the "not found" sentinel
     and chain-integrity violations) stays at exit 1.
   Regression tests: TestVerifyAnnexIvPipelineModeExitCodes — three
   tests covering missing → 1, unreadable → 2, chain-violation → 1.

6) forgelm/compliance.py:_verify_manifest_payload +
   verify_pipeline_manifest_at_path — non-dict stage type-guard
   (valid):
   Both the in-memory verifier *and* the disk wrapper iterated
   manifest["stages"] and called ``s.get(...)`` without checking each
   ``s`` is a dict — a tampered manifest with ``stages: [null,
   "string"]`` would crash with AttributeError instead of producing
   a structured violation.  Added per-item ``isinstance(s, dict)``
   check in both code paths; the in-memory verifier filters the
   well-formed subset out and runs the rest of the checks on that
   filtered list so downstream helpers (which assume dicts) stay
   stable.  Regression test:
   test_disk_wrapper_type_guards_non_dict_stage_items pins both
   surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first node of the flowchart in docs/usermanuals/{en,tr}/training/lora.md
embedded a bare ``<`` inside the rhombus label:

    Q1{Need to fit on a<br/>consumer GPU<br/>(<24 GB)?}

Mermaid 10.9.4 parses the ``<`` as the start of an HTML tag inside a
``{...}`` rhombus node, fails to find a matching ``>``, and aborts the
whole diagram with "Syntax error in text".  The site's user-manual
renderer surfaces this verbatim where the diagram should be — the
exact error the user reported.

Fix: wrap the affected label in double quotes so Mermaid treats the
contents literally:

    Q1{"Need to fit on a<br/>consumer GPU<br/>(<24 GB)?"}

Same fix in the Turkish mirror.  No other mermaid label in the user
manuals uses an unquoted ``<`` — verified by grep across
docs/usermanuals/.

The site/js/usermanuals/*.js bundles are gitignored (regenerated at
CI build via ``tools/build_usermanuals.py``); ``build_usermanuals.py
--check`` is green so the next deploy picks the fix up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Following the LoRA decision-diagram fix (6a6ca60), a scan of every
mermaid block under docs/usermanuals/, docs/guides/, docs/reference/,
docs/qms/, docs/standards/, and docs/roadmap*/ surfaced 5 more node
labels with unquoted special characters that could trip Mermaid's
parser depending on version.  Fix in all of them: wrap the label in
``"…"`` so Mermaid takes the contents literally — same approach as
the LoRA fix.

Identified by a regex scanner that walks each mermaid fence, parses
out the shape brackets, strips safe ``<br/>`` tokens, and flags any
remaining bare ``<`` / ``>`` / ``&`` in the label.  Scanner re-runs
clean after these fixes.

Files:

- docs/usermanuals/en/getting-started/introduction.md:35
  ``I[Export &<br/>deploy]`` — bare ``&`` immediately before
  ``<br/>`` reads as a partial HTML entity start.  TR mirror uses
  "ve" instead of "&" and was already safe.
- docs/usermanuals/{en,tr}/concepts/alignment-overview.md:13
  ``SFT[SFT<br/>format & content]`` (EN) and
  ``SFT[SFT<br/>format & içerik]`` (TR) — same bare-``&`` risk.
- docs/reference/data_ingestion_architecture{,_tr}.md ~L210
  ``CHECK{>= 70 percent of pages?}`` (EN) and
  ``CHECK{>= yüzde 70 sayfa?}`` (TR) — bare ``>=`` at the start of a
  rhombus label can confuse Mermaid because ``>`` overlaps with the
  asymmetric-shape syntax.
- docs/roadmap/completed-phases.md:425
  ``F{> threshold?}`` — same bare-``>`` issue.

User-manual bundles regenerated via ``tools/build_usermanuals.py``
(the site/js/usermanuals/*.js bundles are gitignored — CI rebuilds
on deploy).  Bilingual parity + anchor-resolution guards green.

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

Two findings from the latest Sonar pass on a65d3c9:

forgelm/cli/_pipeline.py:550 — run() cognitive complexity 22 → ≤15
  After the previous refactor (a65d3c9, 53 → ≤15) the for-loop body
  still contributed ~7 to the score: three nested skip checks, two
  post-execution conditional updates, and one ``should_break`` guard
  all sat inside a for inside the method.

  Lifted the entire stage loop into ``_run_stage_loop`` and the
  skip-decision into ``_stage_skip_reason`` (returns a discriminated
  string for "filtered" / "already_completed" / "chain_broken" /
  None).  Inline the two ``if x: y = z`` post-execution updates as
  ternary / boolean-or expressions:

    prev_output = new_prev_output if new_prev_output is not None else prev_output
    chain_broken = chain_broken or chain_broken_now

  ``run()`` is now linear: prepare state → resume skiplist → filter
  setup → started event → loop → finalise → summary.

tests/test_pipeline_compliance.py:496 — try/except SystemExit (S5754)
  The ``_run`` helper in TestVerifyAnnexIvPipelineModeExitCodes wraps
  ``_run_verify_annex_iv_cmd`` in a bare ``try/except SystemExit`` to
  capture the CLI's exit code.  Sonar flagged the catch as the
  "swallow without reraise" anti-pattern (BaseException subclass —
  ``SystemExit`` inherits from BaseException, not Exception).

  Replaced with ``pytest.raises(SystemExit)`` — pytest's idiomatic
  context manager for CLI tests.  The contract is now positive
  ("we *expect* SystemExit every invocation") rather than
  speculative-catch, and a future code path that fails to ``sys.exit``
  would surface as a test failure rather than being silently masked.

Verification:
- ruff format + check clean
- 154 / 154 pipeline + webhook tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent final-review reviewers spotted 6 findings; both were
verified against HEAD before any change.  All 6 are addressed.

🔴 F-B-1 (Blocking) — Pipeline audit events got different run_ids
   Verified: AuditLogger.__init__ accepts an optional run_id; without
   one it auto-generates ``fg-<uuid>``.  ``_audit_event`` constructed
   a fresh AuditLogger per call, so every entry in the pipeline-level
   ``audit_log.jsonl`` carried a different top-level ``run_id`` field
   even though the events shared the same ``pipeline_run_id`` payload.
   That breaks SIEM-style grouping and the Article 12 record-keeping
   correlation contract — auditors join on ``run_id``.

   Fix: cache an AuditLogger on the orchestrator, pinned to the
   pipeline run id, lazily initialised on the first event.
   ``_audit_event`` now requires ``pipeline_run_id`` as a keyword-only
   argument (all 8 existing call sites already pass it, so the new
   signature is back-compat-safe and prevents silent regression).
   Regression test: test_audit_events_share_one_pipeline_run_id.

🔴 F-B-2 (Blocking) — --stage <non-first> manifests fail the chain verifier
   Verified: ``--stage dpo_stage`` with the predecessor's
   ``final_model`` on disk runs correctly (auto-chains from the
   on-disk seed) but the predecessor's manifest entry has
   ``status: skipped_by_filter`` and ``output_model: null``.  The
   executed stage records ``input_source: "chain"``, so the strict
   chain verifier (F-B-3 from an earlier cycle) compares against
   ``stages[X-1].output_model = null`` and reports
   ``chain_integrity_violation: previous stage has no output_model``
   on every legitimate ``--stage <non-first>`` run.

   Fix: after ``_resolve_stage_filter_setup`` returns the resolved
   disk seed, surface it on the predecessor's
   ``state.stages[filter_idx - 1].output_model`` so the manifest
   accurately represents "stage X-1 produced this path (in a prior
   run); stage X chained from it now".  Strict chain verifier is
   happy; stage X-1's status stays ``skipped_by_filter`` to convey
   "we didn't run it in this invocation".  Regression test:
   test_filter_middle_stage_with_disk_seed_passes_chain_verifier.

🟡 F-S-1 (Significant) — Pipeline state writes raced on shared .tmp
   Verified: ``_atomic_write_json`` used ``f"{path}.tmp"`` —
   shared filename per target.  Two writers truncate each other's
   tmp and lose the race to ``os.replace`` with FileNotFoundError.
   Phase 14 explicitly doesn't support concurrent orchestrators on
   the same ``pipeline.output_dir`` (documented now under
   Limitations), but the FileNotFoundError traceback was a worse
   failure mode than last-writer-wins.

   Fix: switch to ``f"{path}.{pid}.{rand}.tmp"`` for the temp name.
   Each writer gets its own tmp file, atomically replaces.  Added a
   defensive cleanup on write failure (orphan tmp removed; original
   exception re-raised).  The temp lives in the same directory as
   the target so ``os.replace`` stays a same-filesystem atomic
   rename.

🟢 F-N-1 (Nitpick) — CHANGELOG audit-event count was stale
   The Added section said "5 new audit events" enumerating the
   original five; HEAD emits 7 (added stage_gated in F-N-1 and
   force_resume in F-B-2 from earlier cycles).  Updated the count
   + list; also called out the F-B-1 run_id-pinning fix.

🟢 F-N-2 (Nitpick) — Limitations doc gaps
   docs/guides/pipeline.md + pipeline-tr.md gained three new bullets:
   - No multi-process locking on pipeline.output_dir (concurrent
     orchestrators race; pick distinct dirs).
   - Stage-vs-pipeline output_dir overlap is allowed but interleaves
     the audit log (matches the inheritance_matrix.yaml fixture's
     layout — documented, not a bug).
   - --force-resume topology guard catches positional breaks only;
     value-level edits (trainer_type, output_dir, hyperparameters
     under identical names) are accepted by design.

🟢 F-N-3 (Nitpick) — _validate_resume_state docstring clarity
   The topology-guard comment now spells out that value-level edits
   are accepted by design — the operator's explicit signal under
   ``--force-resume`` that they understand the divergence.  Aligns
   the code's contract with the guide's Limitations bullet.

Verification:
- ruff format + check clean
- 159 / 159 pipeline + webhook tests pass (new audit-run_id +
  --stage-disk-seed tests added)
- All 11 self-review guards green

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

Copy link
Copy Markdown
Collaborator Author

Final review round — commit cd9f416

Two independent final-round reviewers (one Approve with nitpicks, one Block with 2 blockers + 1 significant) both verified against HEAD. 6 findings; all addressed.

Blocking — Reviewer 2

🔴 F-B-1 — Pipeline audit events had different top-level run_id per event
AuditLogger.__init__ accepts an optional run_id; without one, generates fg-<uuid>. _audit_event constructed a fresh logger per call, so every pipeline-level audit_log.jsonl entry carried a different top-level run_id even though pipeline_run_id payload field was consistent. Breaks SIEM-style grouping and Article 12 record-keeping correlation contract.

Fix: cache AuditLogger on the orchestrator, pinned to the pipeline run id at first event. _audit_event now requires pipeline_run_id as a keyword-only argument (all 8 call sites already pass it). Regression test test_audit_events_share_one_pipeline_run_id.

🔴 F-B-2 — --stage <non-first> manifests fail the chain verifier
With predecessor's final_model on disk: predecessor's manifest entry got status: skipped_by_filter + output_model: null; executed stage got input_source: "chain". F-B-3's strict chain verifier (earlier cycle) flagged chain_integrity_violation on every legitimate --stage <non-first> run.

Fix: after _resolve_stage_filter_setup returns the resolved disk seed, surface it on state.stages[filter_idx-1].output_model. The predecessor's skipped_by_filter status stays (conveys "we didn't run it in this invocation"); its output_model now accurately reflects "we used this prior-run output as the chain seed". Regression test test_filter_middle_stage_with_disk_seed_passes_chain_verifier.

Significant — Reviewer 2

🟡 F-S-1 — _atomic_write_json raced on shared .tmp
f"{path}.tmp" was a shared filename per target. Two writers truncate each other's tmp and lose the race. Phase 14 doesn't support concurrent orchestrators on the same pipeline.output_dir (now documented), but the FileNotFoundError traceback was a worse failure mode than last-writer-wins.

Fix: f"{path}.{pid}.{rand}.tmp" per-writer-unique tmp + cleanup-on-failure that re-raises the original exception.

Nitpicks — both reviewers

🟢 F-N-1 — CHANGELOG audit-event count stale (5 → 7). Lists pipeline.stage_gated + pipeline.force_resume now.

🟢 F-N-2 — docs/guides/pipeline.md + TR mirror Limitations. Three new bullets: (1) no multi-process locking on pipeline.output_dir, (2) stage-vs-pipeline output_dir overlap is allowed but interleaves the audit log (matches inheritance_matrix.yaml fixture), (3) --force-resume topology guard catches positional breaks only — value-level edits accepted by design.

🟢 F-N-3 — _validate_resume_state docstring. Spells out that value-level edits (trainer_type, training.output_dir, hyperparameters) are accepted under --force-resume by design.

Out of scope (acknowledged by Reviewer 1 as nitpicks for v0.7.0)

  • AuditLogger reconstruction overhead — subsumed by the F-B-1 fix above (cached on orchestrator).
  • Same-output_dir collision: now documented; explicit dry-run guard for the pipeline.output_dir == stage.output_dir case would be a UX improvement, not a correctness fix.

Verification

  • ruff format + check clean
  • 159 / 159 pipeline + webhook tests pass (new audit-run_id + --stage-disk-seed tests)
  • All 11 self-review guards green

A single CRITICAL python:S5754 finding on cd9f416's new atomic-write
race regression test: ``except BaseException`` is the documented
anti-pattern (SystemExit / KeyboardInterrupt should propagate).

Narrowed to ``except Exception`` — still wide enough to capture any
unexpected escape from ``_atomic_write_json`` for the race-probe
assertion, but doesn't shadow SystemExit / KeyboardInterrupt the way
``BaseException`` does.  The race the test actually targets is an
OSError (FileNotFoundError from ``os.replace`` losing to a concurrent
unlink), well within ``Exception``.  Test still passes.

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

Copy link
Copy Markdown

@cemililik
cemililik merged commit 2c6c741 into development May 14, 2026
4 checks passed
cemililik added a commit that referenced this pull request May 14, 2026
…ng site

Phase 14 multi-stage pipelines (PR #53) merged into development without
the promotional / discoverability surface around it. This commit lands
that surface so operators looking for "chain my trainers together"
actually find the feature.

Scope (single content drop — no behavioural changes):

1. README.md — added "Multi-Stage Pipelines" row to the Guides table
   between Alignment and CI/CD Pipeline Integration, with EN + TR
   guide links.

2. site/features.html — added a "Multi-Stage Pipelines" feature card
   in the Training & Alignment group (between GRPO and QLoRA), with a
   chain-icon SVG and short copy: "Chain SFT → DPO → GRPO in one
   config-driven run. Auto-chained inputs, per-stage gates,
   crash-safe resume, one Annex IV manifest covering the whole chain."

3. site/js/translations.js — new `features.train.pipeline.{title,body}`
   i18n keys in all 6 languages (EN/TR/DE/FR/ES/ZH) for the card.

4. site/index.html — new 4th hero slide (after compliance) using new
   `home.hero.pipeline.*` keys. Title "SFT → DPO → GRPO, one config.",
   CTAs to the pipeline guide + YAML example.  Slider auto-detects
   the new slide (DOM-driven in main.js); new pagination dot added
   with localised aria-label.

5. site/js/translations.js — new `home.hero.pipeline.{title,lead,
   cta1,cta2}` + `home.hero.dot.pipeline` i18n keys in all 6 languages.
   Note: pre-existing orphan `home.pipeline.*` keys (trainer-stack
   pitch from a prior section) are left intact; they describe a
   different concept (six paradigms) and a separate marketing pass
   can decide whether to wire or remove them.

6. docs/usermanuals/{en,tr}/training/pipelines.md — new sidebar page
   under Training & Alignment, mirroring the structure of dpo.md /
   grpo.md (mermaid decision tree, minimal config, CLI surface,
   what-the-orchestrator-gives-you, inheritance matrix, limitations,
   see-also). Sources of truth remain `docs/guides/pipeline{,-tr}.md`;
   the manual page is the discovery surface inside the sidebar.

7. docs/usermanuals/_meta.yaml — registered the new page (`id:
   pipelines`) between `grpo` and `lora` in the Training & Alignment
   section, with localised titles in all 6 languages.

8. docs/usermanuals/{en,tr}/concepts/choosing-trainer.md — added a
   short note under "Common starting points" pointing every "→"-shaped
   trainer sequence at the new pipelines page, plus a "See also" entry.

9. site/js/usermanuals/{en,tr,_index}.js — regenerated via
   tools/build_usermanuals.py (66 pages per language, up from 65).

Validation (all green):
- ruff format + check
- 11-guard gauntlet (dry-run, bilingual parity, anchor resolution,
  CLI help consistency, wizard defaults, no-analysis-refs,
  unguarded-sys-modules-pop, site-version, build_usermanuals --check)
- 180 / 180 tests across pipeline + bilingual-parity + anchor-resolution

Out of scope (deferred):
- CHANGELOG move from [Unreleased] → [0.7.0] — release will be cut
  after this surface refresh is approved.
- Wizard (CLI + web) pipeline support — Phase 14 explicit out-of-scope.
- Old `home.pipeline.*` orphan i18n keys — separate marketing pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 15, 2026
… 2 MEDIUM)

Each finding was verified against current code; 10 were applied as
minimal edits, 4 documented as out-of-scope for the release-cut PR.

🔴 BLOCKER 1 — Pipeline dispatch silently overrode no-train flags
   File: forgelm/cli/_pipeline.py
   Verified: ``_dispatch_pipeline_mode`` routes BEFORE
   ``_maybe_run_no_train_mode`` (the F-B-1 fix from PR #53 was
   explicit about this), so ``--fit-check`` / ``--merge`` /
   ``--generate-data`` / ``--compliance-export`` / ``--benchmark-only``
   on a pipeline config would silently trigger a full pipeline
   training run instead of the single-stage operation requested.
   Fix: ``run_pipeline_from_args`` now rejects each of the five
   unsupported single-stage flags with EXIT_CONFIG_ERROR + an
   actionable error pointing operators at ``--dry-run`` for
   multi-stage validation.  ``--dry-run`` itself still routes to
   ``orchestrator.dry_run()``.  Regression test:
   ``test_no_train_single_stage_flags_rejected_on_pipeline_config``.

🔴 BLOCKER 2 — --resume-from did not skip gated_pending_approval
   File: forgelm/cli/_pipeline.py:376
   Verified: ``_resolve_resume_skiplist`` only skipped stages with
   ``status == "completed"``.  A stage that exited with
   EXIT_AWAITING_APPROVAL is recorded as ``gated_pending_approval``.
   After ``forgelm approve`` promotes the staging path and the
   operator resumes from a later stage, the gated SFT would re-train,
   DPO would auto-chain from the freshly-trained output instead of
   the operator-approved one, and the audit log would carry two
   ``pipeline.stage_started`` events for the same stage_index.
   Fix: skip set now contains both ``completed`` and
   ``gated_pending_approval`` — presence of an ``output_model``
   directory on disk is what proves the stage produced a real
   artefact, regardless of which status label it carries.
   Regression test:
   ``test_resume_skips_gated_pending_approval_with_on_disk_output``.

🔴 BLOCKER 3 — Topology guard skipped on hash match
   File: forgelm/cli/_pipeline.py:909
   Verified: ``_validate_resume_state`` returned ``None`` immediately
   when ``existing.pipeline_config_hash == self.config_hash``, before
   the topology check ran.  A tampered / truncated / hand-edited
   ``pipeline_state.json`` could carry the same config_hash (operator
   never edited the YAML) but a damaged ``stages[]`` array, and the
   resume would silently overwrite the wrong stage's history because
   ``state.stages[i]`` is positionally addressed.
   Fix: topology check (ordered ``[s.name for s in stages]`` lists)
   now runs UNCONDITIONALLY — before the hash shortcut.  Regression
   test: ``test_topology_guard_runs_even_on_hash_match``.

🟠 HIGH 4 — output_dir collision check only in dry_run()
   File: forgelm/cli/_pipeline.py:770
   Verified: the per-stage ``training.output_dir`` collision guard
   only fired during ``--dry-run``; an operator who skipped dry-run
   could silently overwrite per-stage manifests + checkpoints across
   stages.
   Fix: extracted the collision check into
   ``_check_output_dir_collisions`` and now call it as a pre-flight
   in ``run()`` too.  Regression test:
   ``test_output_dir_collision_rejected_in_run_not_only_dry_run``.

🟠 HIGH 5 — Pipeline-only flags silently ignored on non-pipeline configs
   File: forgelm/cli/_dispatch.py:300
   Verified: ``--stage`` / ``--resume-from`` / ``--force-resume`` /
   ``--input-model`` against a config with no ``pipeline:`` block
   was a no-op — operator surprise.
   Fix: ``_main_inner`` now rejects each pipeline-only flag with
   EXIT_CONFIG_ERROR + an actionable error.  Regression test:
   ``test_pipeline_only_flags_rejected_on_non_pipeline_config``.

🟠 HIGH 8 — Reference docs missing the pipeline: block + new CLI flags
   Files: docs/reference/configuration.md, configuration-tr.md,
          usage.md, usage-tr.md
   Verified: docs/standards/documentation.md mandates that config /
   CLI surface changes are mirrored in the canonical reference docs
   (EN + TR) before merge.  Phase 14's ``pipeline:`` block and the
   four new CLI flags were not yet documented.
   Fix: appended a "pipeline (Optional — Multi-Stage Training
   Chains, Phase 14)" section to ``configuration.md`` + TR mirror
   covering the schema (root + PipelineStage fields, inheritance
   rules, root-only sections, YAML example, CLI surface, verifier
   command).  Added a "Multi-Stage Pipelines (Phase 14, v0.7.0)"
   subsection to ``usage.md`` + TR mirror with the 7 CLI invocations
   and the single-stage-flag-rejection contract.  Cross-linked both
   directions.

🟠 HIGH 9 — Module-size grandfather list policy was a frozen v0.6.x set
   File: tools/check_module_size.py
   Verified: the file-level docstring claimed the grandfather list
   was specifically "the seven modules that already sit over the
   ceiling at PR #29 HEAD"; adding ``forgelm/cli/_pipeline.py`` to
   that list without acknowledging the policy change weakens the
   guard's discipline.
   Fix: rewrote the grandfather-policy section of the docstring to
   make the admission criteria explicit (inline-comment requirement
   + follow-up tracking artefact) and called out the post-PR-#29
   addition (``_pipeline.py``).  The discipline IS preserved — every
   entry has a tracking pointer.

🟡 MEDIUM 12 — JSONDecodeError conflated with OSError in pipeline verifier
   Files: forgelm/compliance.py, forgelm/cli/subcommands/_verify_annex_iv.py
   Verified: ``verify_pipeline_manifest_at_path`` emitted a shared
   "unreadable" sentinel for both ``OSError`` and ``JSONDecodeError``,
   so the CLI mapped malformed JSON to EXIT_TRAINING_ERROR (exit 2)
   even though parse failures are operator-actionable input errors
   (the single-artefact path maps them to exit 1).
   Fix: verifier now emits a distinct ``invalid JSON`` sentinel for
   ``JSONDecodeError`` and keeps ``unreadable`` for ``OSError`` only.
   CLI maps ``invalid JSON`` → EXIT_CONFIG_ERROR (1), ``unreadable``
   → EXIT_TRAINING_ERROR (2).  Existing test that asserted exit 2
   on malformed JSON was a stale carry-forward; updated to match
   the corrected contract.

🟡 MEDIUM 13 — Roadmap state inconsistent with the cut release
   Files: docs/roadmap.md, docs/roadmap-tr.md,
          docs/guides/pipeline.md, docs/guides/pipeline-tr.md
   Verified: Phase 14 row still said "📋 Planned" and the
   ``Current state:`` line still said "Phase 14 planned post-release"
   while CHANGELOG.md is dated [0.7.0] — 2026-05-14 and pyproject.toml
   reports version 0.7.0.  The pipeline guide's header said
   "currently on the Unreleased line of the CHANGELOG" which is no
   longer true.
   Fix: Phase 14 row → "🟡 Merged — implementation complete on
   `development`; `v0.7.0` PyPI tag is the release-pending step".
   Current-state and tree-comment lines updated to match.  Guide
   header → "landing in `v0.7.0` (CHANGELOG entry dated 2026-05-14;
   PyPI tag pending)".  TR mirrors updated in lockstep.  The full
   "Done" promotion to ``completed-phases.md`` lands as part of the
   post-tag dev-cycle reset commit (per cut-release skill step 4).

🟡 MEDIUM 14 — New pipelines.md pair not in bilingual parity registry
   File: tools/check_bilingual_parity.py
   Verified: docs/usermanuals/{en,tr}/training/pipelines.md added in
   the docs+site surface-refresh commit (d404092) but the
   heading-spine parity check (``_PAIRS`` registry) wasn't extended,
   leaving the pair open to structural drift.
   Fix: registered the pair in ``_PAIRS``.  The check now reports
   "48 bilingual doc pair(s)" (was 47).

⏭️ Skipped for v0.7.0 cut (documented for v0.7.x follow-up)
---
- HIGH 6 — canonical pipeline manifest hash + non-chain-field tamper
  detection: real but material design change (needs spec work +
  verifier overhaul on top of the existing Annex IV scheme).  Not a
  release blocker because (a) the structural verifier already catches
  the most common tamper shapes (chain integrity, stage indexing,
  status consistency), (b) the per-stage manifests retain their
  existing canonical hashes, and (c) the pipeline manifest is
  written atomically per transition and the surrounding audit log
  is hash-chained — a non-chain-field tamper window is narrow.
  Tracked for v0.7.x.

- HIGH 7 — disk verifier deep-parses each per-stage training_manifest:
  same complexity class as HIGH 6.  Currently the disk-wrapper checks
  ``os.path.isfile(per_stage_manifest)``; deep parsing would embed
  the full Annex IV single-artefact verifier per stage.  Filed as
  v0.7.x; pairs with HIGH 6 to land together as a "pipeline manifest
  verification rewrite" sub-phase.

- MEDIUM 10 — webhook ``pipeline.*`` events extend the 5-event
  vocabulary: events were intentionally added with a documented
  rollout (CHANGELOG enumerates 7 new ``pipeline.*`` events; the
  webhook contract's ``event`` field is an open-ended string, not
  an enum; receivers that hard-validate against a frozen enum need
  to be updated, but the schema explicitly leaves room).  Defer
  unless a downstream consumer reports the issue.

- MEDIUM 11 — ``_send(**extra)`` allowlist: ``**extra`` is
  caller-controlled (orchestrator methods that already control the
  call site), not from external input, and the base-field-collision
  guard prevents the obvious attack.  Tightening to an explicit
  allowlist is good hygiene but not a release blocker.  Tracked for
  v0.7.x.

Verification
---
- ruff format + check clean
- 222 / 222 tests pass (pipeline + compliance + CLI + dispatch +
  bilingual parity + anchor resolution + module size + cli
  subcommands)
- All 11 doc guards green in the gauntlet:
  dry-run, site-version --check, bilingual parity (48 pairs),
  anchor resolution (274 markdown files), CLI help consistency
  (457 invocations), wizard defaults, no-analysis-refs,
  no-unguarded-sys-modules-pop, build_usermanuals --check, ruff,
  module-size (--strict, 7 grandfathered)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 15, 2026
publish.yml run 25906380797 (v0.7.0 tag push) failed on all four
Windows matrix cells (3.10/3.11/3.12/3.13) of cross-os-tests with:

  AssertionError: Concurrent writers produced 12 exception(s):
    [PermissionError(13, 'Access is denied'), ...]
  tests/test_pipeline_orchestrator.py:246

The test
(``TestAtomicWriteJson.test_per_writer_temp_path_does_not_race_on_shared_tmp``)
pins the POSIX ``FileNotFoundError`` race that the F-S-1 fix on PR #53
addressed: pre-fix the helper used a shared ``<target>.tmp`` and
``os.replace`` from writer B could fail because writer A had just
unlinked the same tmp via its own rename.  Per-writer-unique tmp
filenames eliminate that bug on POSIX.

On Windows, ``os.replace`` to the same destination path raises a
DIFFERENT error (``PermissionError: Access is denied``) when
concurrent writers briefly hold a handle on the destination — that's
a file-lock race intrinsic to the Win32 rename API, not the
tmp-rename race the F-S-1 fix is about.  The pipeline orchestrator's
atomic-write contract is POSIX-shaped (sequential writers; the
test's 16-thread stress is purely a regression probe against the
original race shape), and Windows's concurrent-replace semantics
are not in scope.

Fix: marker ``@pytest.mark.skipif(sys.platform == "win32", ...)``
with a docstring explaining the platform-specific reason.  The
POSIX-side regression coverage is preserved on the ubuntu-latest and
macos-latest matrix cells.

PyPI was NOT published in run 25906380797 — the ``pypi`` job is
gated on ``needs: cross-os-tests`` which never went green.  The
v0.7.0 GitHub tag was deleted (local + remote) so this commit can be
re-tagged onto main HEAD and publish.yml re-fired cleanly.

Verification:
- ruff format --check + ruff check clean
- TestAtomicWriteJson passes locally (2/2; macOS)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 15, 2026
publish.yml run 25906380797 (v0.7.0 tag push) failed on all four
Windows matrix cells (3.10/3.11/3.12/3.13) of cross-os-tests with:

  AssertionError: Concurrent writers produced 12 exception(s):
    [PermissionError(13, 'Access is denied'), ...]
  tests/test_pipeline_orchestrator.py:246

The test
(``TestAtomicWriteJson.test_per_writer_temp_path_does_not_race_on_shared_tmp``)
pins the POSIX ``FileNotFoundError`` race that the F-S-1 fix on PR #53
addressed: pre-fix the helper used a shared ``<target>.tmp`` and
``os.replace`` from writer B could fail because writer A had just
unlinked the same tmp via its own rename.  Per-writer-unique tmp
filenames eliminate that bug on POSIX.

On Windows, ``os.replace`` to the same destination path raises a
DIFFERENT error (``PermissionError: Access is denied``) when
concurrent writers briefly hold a handle on the destination — that's
a file-lock race intrinsic to the Win32 rename API, not the
tmp-rename race the F-S-1 fix is about.  The pipeline orchestrator's
atomic-write contract is POSIX-shaped (sequential writers; the
test's 16-thread stress is purely a regression probe against the
original race shape), and Windows's concurrent-replace semantics
are not in scope.

Fix: marker ``@pytest.mark.skipif(sys.platform == "win32", ...)``
with a docstring explaining the platform-specific reason.  The
POSIX-side regression coverage is preserved on the ubuntu-latest and
macos-latest matrix cells.

PyPI was NOT published in run 25906380797 — the ``pypi`` job is
gated on ``needs: cross-os-tests`` which never went green.  The
v0.7.0 GitHub tag was deleted (local + remote) so this commit can be
re-tagged onto main HEAD and publish.yml re-fired cleanly.

Verification:
- ruff format --check + ruff check clean
- TestAtomicWriteJson passes locally (2/2; macOS)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 15, 2026
v0.7.0 published to PyPI (publish.yml run 25906894834, all 12 cross-OS
matrix cells green, OIDC trusted publish success).  Smoke test:
``pip install forgelm==0.7.0`` in a fresh venv reports
``forgelm --version → ForgeLM 0.7.0``.  Post-release housekeeping:

1. ``pyproject.toml`` → ``0.7.1rc1`` (next dev cycle marker)
2. ``docs/roadmap.md`` + ``-tr.md``:
   - Phase 14 row: "🟡 Merged" → "✅ Done" with v0.7.0 release
     date + completed-phases anchor
   - "Latest release on PyPI" / "PyPI'deki son sürüm" paragraph:
     v0.6.0 → v0.7.0 with the Phase 14 + SSRF hardening summary;
     v0.6.0 demoted to "Previous release" / "Önceki sürüm"
   - "Released:" / "Yayınlandı:" line: v0.6.0 → v0.7.0
   - "Current state" line: 20 phases → 21 phases (Phase 14 added),
     PyPI through v0.7.0
   - Mermaid: ``v0.7.0`` node → ``v0.7.0 ✅ Released`` /
     ``✅ Yayınlandı``
   - Tree-comment for ``phase-14-pipeline-chains.md``: "Merged …
     v0.7.0 tag pending" → "Done — shipped v0.7.0 (PyPI 2026-05-15)"
   - ``releases.md`` comment: ``v0.3.0 → v0.6.0`` → ``v0.3.0 →
     v0.7.0``
3. ``docs/roadmap/completed-phases.md``: appended a comprehensive
   Phase 14 entry (driver, what shipped, security fold-in,
   backward-compat invariant, full review-absorption history with
   PR #53 + #54 round summaries, cross-references to operator
   guides + schema + user manual pages).  Slug
   ``#phase-14-multi-stage-pipeline-chains-v070`` matches the
   target anchor used from ``roadmap.md`` and ``roadmap-tr.md``.

Verification:
- ruff format --check + ruff check clean
- 48 / 48 bilingual doc pair parity green
- 274 markdown files: all anchors + relative links resolve
  (including the new ``completed-phases.md#phase-14-...`` anchors)
- site/ version literals still pin v0.7.0 (no change needed; the
  bump to 0.7.1rc1 is dev-cycle only)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant