Skip to content

feat: Phase 11 + post-v0.4.5 sync (v0.5.0) - #11

Merged
cemililik merged 9 commits into
mainfrom
development
Apr 27, 2026
Merged

cemililik merged 9 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Apr 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR brings the development branch up to v0.5.0 state and lands Phase 11 — Document Ingestion & Data Audit plus post-release housekeeping from v0.4.5.

  • Phase 11 — Document Ingestion (forgelm ingest): Multi-format extractors (PDF / DOCX / EPUB / TXT / Markdown) with paragraph + sliding chunking, optional --pii-mask. Output is {"text": ...} JSONL the trainer accepts. OCR is out of scope; see new guide for Tesseract / AWS Textract handoff.
  • Phase 11 — Dataset Audit (forgelm --data-audit): CPU-only audit producing data_audit_report.json with per-split metrics, top-3 language detection, simhash near-duplicate detection, cross-split leakage check, PII regex (Luhn + TC Kimlik validated), schema-drift detection. Auto-inlines into EU AI Act Article 10 governance artifact when present at training time.
  • 19 review findings applied, 14 deferred to a documented Phase 11.5 backlog (LSH banding, streaming reader, etc.).
  • Post-v0.4.5 sync: roadmap / phase-12-quickstart / releases.md updated to reflect v0.4.5 PyPI publish; pyproject.toml + forgelm/__init__.py bumped to 0.5.0rc1 for the next dev cycle.
  • README: PyPI badge added; all 9 notebooks listed (was 5/9).

Phase 11 — what's new

New modules

  • forgelm/ingestion.py — single file or directory tree → SFT-ready JSONL. Sliding + paragraph chunking, recursive walk, encrypted-PDF detection, binary-content warning, deterministic file ordering.
  • forgelm/data_audit.py — JSONL → audit JSON. Sample count, length distribution (p50/p95), language detection, simhash near-duplicate detection (within-split + cross-split leakage), PII regex with Luhn + TC Kimlik checksum validation, schema-drift detection.

New CLI surface

forgelm ingest ./policies/ --recursive --output data/policies.jsonl
forgelm ingest ./book.epub --strategy sliding --chunk-size 2048 --overlap 200 --output data/book.jsonl
forgelm ingest ./customer_emails/ --pii-mask --output data/anon.jsonl

forgelm --data-audit data/policies.jsonl --output ./audit/
forgelm --data-audit data/  # split-keyed directory

New / updated guides

Optional dependency

pip install -e ".[ingestion]"   # pypdf, python-docx, ebooklib, beautifulsoup4, langdetect

Plain TXT / Markdown ingestion + the full audit module work without the extra (everything is pure stdlib in those paths).

Review findings — applied vs deferred

19 of 34 findings from the post-Phase-11 code review are applied in this PR. Highlights:

# Bug Fix
1 semantic strategy crashed at runtime Removed from --strategy choices
2 --overlap 199 --chunk-size 200 → quadratic chunk count Reject overlap > chunk_size // 2 up front
3 Heterogeneous JSONL columns silently dropped Compute union of keys + schema_drift_columns
6 Phone PII regex over-matched (timestamps, line numbers, ISO dates) Anchored to +CC or (area) formats
7 DE Personalausweis regex matched IATA codes / UUID fragments Narrowed to [A-Z]\d{7,8}[A-Z0-9]?
9 Encrypted PDF surfaced as opaque "extraction failed" Explicit ValueError with qpdf / pdftk hint
10 Binary masquerading as text silently corrupted JSONL Warn at >1% Unicode replacement chars
14 dev.jsonl / eval.jsonl not recognised as splits Alias map → canonical
24 --data-audit --output-format json flooded stdout Summary on stdout, full report to disk
26 Pseudo-split fallback silent Warning + report note
27 Audit report carried only absolute path Added source_input field
30 --pii-mask redacted count not surfaced IngestionResult.pii_redaction_counts per category
31 One bad split aborted whole audit Per-split tolerance: skip + report under error key
34 Audit notes informational, not actionable Notes now suggest forgelm ingest --pii-mask etc.

Plus three "green tier" follow-ups landed in this PR:

  • ebooklib options= deprecation silenced
  • Audit progress logging (per-split open + every-5000-rows simhash + near-dup start)
  • OCR handoff documentation (Tesseract + AWS Textract worked recipes)

12 items remain in phase-11-5-backlog.md — LSH banding, streaming reader, token-aware chunk sizing, PII severity tiers, etc.

Other commits in this PR

  • 47232fc — post-v0.4.5 release sync (roadmap / phase-12-quickstart / releases reflect PyPI publish; bump to 0.5.0rc1)
  • ed81e09 — README: PyPI badge + all 9 notebooks listed (was 5/9)

Test plan

  • ruff format . + ruff check . — clean
  • pytest tests/ — 718 passed, 11 skipped (8 pre-existing TRL/torch FSDP skips, 3 PDF roundtrip skips when pypdf not installed)
  • Manual smoke: forgelm ingest TXT_DIR + forgelm --data-audit JSONL end-to-end
  • Progress logging verified on 6000-row audit (split open + simhash progress + near-dup start lines fire at expected intervals)
  • Phase 11 nightly CI smoke added (TXT-only, no extras required)
  • CLI --help regression — every subcommand still parses
  • Reviewer: spot-check forgelm ingest --pii-mask on a sample with mixed PII; verify counts in IngestionResult.pii_redaction_counts
  • Reviewer: confirm pip install -e ".[ingestion]" in a fresh venv pulls all 5 deps (pypdf, python-docx, ebooklib, beautifulsoup4, langdetect)

Notes for reviewer

  • This PR is large because Phase 11 is the next planned phase post-v0.4.5; everything is additive (no breaking changes to existing trainers, configs, or CLI surface).
  • 0.5.0rc1 is set in pyproject.toml so a release-engineering pass after merge can drop rc1 and tag.
  • The 14 deferred review items are explicit choices — see the backlog file for the rationale per item.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Document ingestion CLI: recursive multi-format → SFT-ready JSONL with paragraph/sliding chunking and optional PII masking.
    • Dataset audit CLI: produces data_audit_report.json with per-split quality metrics, language detection, simhash near-duplicate and cross-split leakage reports.
    • Audit findings auto-inlined into governance/export bundles.
  • Documentation

    • New ingestion and data-audit guides, quickstart updates, runnable notebooks, and translated guides.
  • Tests

    • Comprehensive ingestion and audit test suites.
  • Chores

    • Version bumped to 0.5.0rc1 and new ingestion optional-deps extra.
  • CI

    • Nightly end-to-end ingest+audit smoke check.

v0.4.5 published to PyPI on 2026-04-26
(https://github.com/cemililik/ForgeLM/releases/tag/v0.4.5).

Doc state now reflects that:
- roadmap.md / roadmap-tr.md: 'Latest release on PyPI: v0.4.5' (was
  v0.4.0); v0.4.0 demoted to 'Previous'.
- phase-12-quickstart.md: status banner 'shipped as v0.4.5 on
  2026-04-26 (PyPI)' instead of 'merged to main; tag + PyPI pending'.
- releases.md: v0.4.5 status 'Released' with link to GitHub Release.
- doc map line: 'shipped as v0.4.5' (was 'tag + PyPI publish pending').

Next dev cycle: pyproject.toml + forgelm/__init__.py bumped to
0.5.0rc1 (Phase 11 Document Ingestion target). The CHANGELOG
`[Unreleased]` header is already in place and stays empty until the
first 0.5.0 work lands.
The README's Notebooks section listed only 5 of the 9 .ipynb files in
notebooks/. The four missing ones cover real, shippable features that
deserve discoverability:

- post_training_workflow.ipynb — end-to-end Phase 10 toolchain
  (fit-check → chat → export → deploy).
- safety_evaluation.ipynb — 140-prompt × 6-category red-team harness.
- synthetic_data_training.ipynb — teacher-to-student distillation.
- galore_memory_optimization.ipynb — full-parameter training as an
  alternative to LoRA.

Reorganised the section into four logical groups (Getting started /
Alignment / Advanced training / Post-training & safety) with a
one-line description for each non-obvious notebook so a reader can
pick the right one without opening it.

Also:
- Added a PyPI version badge alongside the License/Python/CI badges
  now that v0.4.5 is published.
- Mirrored the notebook list at the bottom of docs/guides/quickstart.md
  so the user-facing guide hands off to the runnable artifacts.
Brings raw enterprise corpora into ForgeLM's training format and surfaces
governance signals before training starts. Bridges ingestion → training →
EU AI Act Article 10 in one tool.

forgelm/ingestion.py + `forgelm ingest` subcommand
- Multi-format extractors: PDF (pypdf), DOCX (python-docx), EPUB
  (ebooklib + beautifulsoup4), TXT, Markdown.
- Two chunking strategies: paragraph (default; greedy, never splits
  mid-paragraph) and sliding (fixed window with --overlap). Semantic
  raises NotImplementedError; reserved for a follow-up phase.
- Output is `{"text": "..."}` JSONL — recognized by data.py as
  pre-formatted SFT input without further preprocessing.
- --recursive walks directory trees; unsupported extensions skip
  silently, supported files with no extractable text skip with a
  warning. OCR is intentionally out of scope.
- --pii-mask redacts emails / phones / Luhn-validated credit cards /
  IBAN / national IDs (TR / DE / FR / US-SSN) before writing.

forgelm/data_audit.py + `forgelm --data-audit` top-level flag
- Per-split metrics: sample count, column schema, length distribution
  (min/max/mean/p50/p95), null/empty rate, top-3 language detection
  (best-effort via langdetect).
- 64-bit simhash near-duplicate detection within each split; default
  Hamming threshold 3 ≈ 95% similarity (canonical web-page-dedup
  setting). Cross-split overlap report catches train-test leakage.
- PII regex set with credit-card Luhn validation and TC Kimlik No
  checksum validation. Per-split + aggregate counts.
- Layout: single .jsonl → treated as `train`; directory containing
  train.jsonl / validation.jsonl / test.jsonl auto-discovered.
- Writes data_audit_report.json under --output (default ./audit/);
  --output-format json mirrors the report on stdout for CI/CD. CPU-only.

EU AI Act Article 10 integration (compliance.py)
- generate_data_governance_report inlines data_audit_report.json under
  the `data_audit` key when present in the trainer's output_dir.
  Compliance bundle becomes a single self-contained document.

pyproject.toml [ingestion] extra
- pypdf, python-docx, ebooklib, beautifulsoup4, langdetect.
  Cross-platform; no native compilation. Plain TXT / Markdown +
  audit module are pure stdlib and work without the extra.

CLI wiring (cli.py)
- New `ingest` subcommand alongside chat / export / deploy / quickstart.
- --data-audit + --output top-level flags; runs before the config-required
  check so operators can audit raw data without writing a YAML.
- Epilog and help text updated with the new subcommand line.

Compatibility
- wizard.py: BYOD dataset prompt now detects directories and hints at
  `forgelm ingest` (was silently re-prompting "not a regular file").
- nightly.yml: added an ingest+audit smoke test (TXT-only path so the
  [ingestion] extra isn't required in CI).

Tests
- tests/test_ingestion.py — TXT path + chunking strategies + PII mask;
  PDF round-trip skips when pypdf missing.
- tests/test_data_audit.py — PII regex + Luhn / TC Kimlik validators,
  simhash properties, end-to-end audit on file + split-keyed directory
  layouts, governance integration.
- tests/test_wizard_byod.py: directory-rejection assertion updated to
  match the new ingest hint.
- 704/704 pass locally (9 GPU/TRL-env skips unchanged).

Documentation
- New: docs/guides/ingestion.md, docs/guides/data_audit.md.
- Updated: README (feature section, install matrix, quickstart hand-off,
  guide table), docs/reference/usage.md (CLI sections), docs/qms/
  sop_data_management.md (audit pipeline integration), docs/standards/
  architecture.md (module table + extras matrix), CHANGELOG.md
  ([Unreleased]), docs/roadmap.md + roadmap-tr.md (Phase 11 → Done),
  docs/roadmap/phase-11-data-ingestion.md (status banner + tasks
  marked [x]), docs/roadmap/releases.md (v0.5.0 entry expanded),
  docs/guides/quickstart.md (Option C: raw docs path).
… backlog

19 findings applied as real fixes; 14 explicitly deferred to a new
docs/roadmap/phase-11-5-backlog.md (LSH index, streaming reader, OCR
docs, etc.); 1 already correct (langdetect seed).

Bugs (functional / safety)
- 1: `semantic` strategy removed from `forgelm ingest --strategy` choices.
  The function still raises NotImplementedError; the CLI surface no longer
  exposes a runtime-crashing option. Help text now points at the
  follow-up phase.
- 2: `_chunk_sliding` rejects `overlap > chunk_size // 2` up front. A typo
  like `--chunk-size 200 --overlap 199` previously emitted ~one chunk
  per character (multi-million-line JSONL) silently.
- 3: `_audit_split` now records the union of keys across rows under
  `columns`, plus a `schema_drift_columns` list when later rows add
  fields not present in row 0. Heterogeneous BYOD JSONL no longer
  loses optional columns silently.
- 6: phone PII regex narrowed. Was matching any 6-12 digit run with
  separators (timestamps, line numbers, ISO dates, IBAN fragments).
  Now anchored to `+CC` country prefix or `(area)` parenthesised area
  code — bare digit runs do not flag.
- 7: `de_id` regex narrowed from `[A-Z0-9]{9,10}` to
  `[A-Z]\d{7,8}[A-Z0-9]?` so IATA codes / UUID fragments / API key
  fragments do not trip Personalausweis detection.
- 8: PDF happy-path test added (hand-built minimal text-bearing PDF; no
  reportlab dep). Encrypted PDF rejection test added.
- 9: `_extract_pdf` distinguishes encrypted PDFs from "extraction failed"
  errors. Owner-encrypted PDFs are auto-decrypted with an empty
  password; user-encrypted PDFs raise ValueError with an actionable
  hint (qpdf / pdftk).
- 10: `_extract_text` warns when more than 1% of a `.txt` file decodes
  as Unicode replacement chars (binary masquerading as text).
- 14: split alias map (`dev`, `val`, `valid`, `eval`, `holdout`)
  collapses onto canonical `validation` / `test`. Cross-split leakage
  works without forcing operators to rename their files.
- 24: `--data-audit --output-format json` now emits a *summary* on
  stdout (top-level metrics + report path), not the full JSON.
  Multi-split audits used to dump tens of KB to CI logs.
- 26: pseudo-split fallback emits a clear warning + report note
  ("cross-split leakage analysis is meaningless without a real
  partition") instead of silently treating arbitrary `*.jsonl` files
  as splits.
- 27: `AuditReport` now carries both `source_path` (absolute, traceable)
  and `source_input` (literal user input, portable across machines).
- 30: `IngestionResult.pii_redaction_counts` records per-category
  redaction counts when `--pii-mask` runs, plus a notes-line summary.
  Compliance evidence ("X emails + Y phones removed") is now visible
  without diffing the masked output.
- 31: `audit_dataset` tolerates per-split read failures (OSError) —
  the bad split lands under `splits[name]["error"]`, the audit
  continues, and a notes line surfaces the skip.
- 34: audit notes now actionable. PII note suggests
  `forgelm ingest --pii-mask` / `mask_pii`; cross-split leakage note
  recommends re-shuffling; near-duplicate note explains the cost.

Doc-only clarifications
- 11: `mask_pii` docstring documents the first-match-wins precedence
  (most-specific patterns scanned first; phone last).
- 15: ingestion guide explains char vs token: `--chunk-size 2048` ≈
  500-700 tokens; size against `model.max_length`.
- 16: `_PII_PATTERNS` order rationale now in module-level comment.
- 18: ingestion guide notes lexicographic file ordering for
  reproducibility.
- 25: `--recursive` help text spells out that the default is shallow.

Tests
- TestSlidingChunking: `test_overlap_above_half_chunk_rejected`
- TestPdfExtractor: text-bearing happy path + encrypted-PDF rejection
- TestPiiDetection: phone narrowing (with-prefix + bare-digits-not-phone),
  de_id narrowing
- TestAuditDirectoryLayout: alias parametrize (dev/val/eval/holdout),
  pseudo-split warning
- TestSchemaDrift, TestPartialFailureTolerance, TestActionableNotes,
  TestReproducibility — new test classes covering Bugs 3 / 31 / 34 / 27.
- 718/718 passing locally (11 skips: 8 pre-existing TRL/torch, 3
  optional-dep PDF roundtrips when pypdf is missing).

Backlog
- New docs/roadmap/phase-11-5-backlog.md captures the 14 deferred
  follow-ups (LSH banding, streaming reader, token-aware chunk size,
  PDF header/footer dedup, simhash token cache, xxhash backend,
  audit progress bar, audit-as-subcommand, PII severity tiers,
  truncation policy, structured ingestion notes, wizard "ingest first"
  entry, OCR handoff docs, ebooklib deprecation).
… progress, OCR docs)

Three small Phase 11.5 backlog items pulled forward — none introduce
breaking changes, none require new dependencies.

ebooklib deprecation (#4 in backlog)
- _extract_epub now passes options={"ignore_ncx": True,
  "ignore_missing_css": True} to epub.read_epub. Silences the
  deprecation / NCX / CSS warnings that newer ebooklib (>=0.18) emits
  when the keyword is omitted; behavior is unchanged for downstream.

Audit progress logging (#7 in backlog)
- _audit_split now emits a "computing simhashes for N rows" notice and
  a "X / N rows fingerprinted" line every 5000 rows when the split
  crosses that threshold. Quickstart-sized audits stay quiet.
- audit_dataset main loop logs each split's open ("scanning split 'X'
  (N rows from path.jsonl)") and a "scanning for near-duplicates
  (O(n²); N rows)" notice when the within-split pairing step is the
  expensive piece. Closes the 3-5 minute silent stretch on 30K+ row
  audits without pulling tqdm in.

OCR handoff documentation (#13 in backlog)
- docs/guides/ingestion.md: new "Working with scanned PDFs" section
  with two worked recipes — Tesseract (local, free, via ocrmypdf or
  raw pdftoppm + tesseract) and AWS Textract (cloud, paid, better on
  multi-column / forms). Plus a brief PII-redaction-after-OCR flow.
  Removes the dead-end where "OCR is out of scope" left operators
  with nowhere to go.

Backlog
- docs/roadmap/phase-11-5-backlog.md updated: the three landed items
  removed from the table, header gains a "Recently landed" callout
  pointing at the relevant docs path. Twelve items remain in the
  follow-up queue.

Verified: ruff clean, 718 tests / 11 skips unchanged, manual smoke
on 6000-row audit confirms progress lines fire at the expected
intervals.

@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, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

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

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7070de3d-f09e-4777-ba10-ac18849f329c

📥 Commits

Reviewing files that changed from the base of the PR and between ebfcd3d and 13c3bdb.

📒 Files selected for processing (4)
  • docs/guides/data_audit.md
  • docs/guides/ingestion.md
  • forgelm/ingestion.py
  • tests/test_ingestion.py
📝 Walkthrough

Walkthrough

This PR delivers Phase 11 "Document Ingestion & Data Audit" for v0.5.0: adds forgelm ingest to convert PDF/DOCX/EPUB/TXT/Markdown into SFT-ready JSONL with paragraph/sliding chunking and optional PII redaction, and adds forgelm --data-audit to emit dataset quality/governance reports (language, simhash dedup, cross-split leakage, PII counts) with trainer inlining support for EU AI Act Article 10.

Changes

Cohort / File(s) Summary
Core Ingestion Module
forgelm/ingestion.py
New module implementing multi-format extraction (.pdf, .docx, .epub, .txt, .md), paragraph and sliding-window chunking, deterministic recursive traversal, optional ingest-time PII masking, JSONL {"text": ...} output, IngestionResult dataclass and summarizers.
Core Data Audit Module
forgelm/data_audit.py
New audit pipeline exposing AuditReport and audit_dataset() for single-file or split-directory inputs; computes per-split text stats, schema-union drift, optional language detection, regex+validator PII detection (Luhn/TR ID checks), 64-bit simhash fingerprinting and near-duplicate Hamming comparisons, cross-split leakage metrics, operator notes, and can write data_audit_report.json.
CLI Integration
forgelm/cli.py
Adds ingest subcommand (chunk controls, --overlap, --strategy, --pii-mask, --recursive) and top-level --data-audit plus --output; dispatches to ingest_path/audit_dataset, supports human/JSON summary output, and maps common failures to exit codes.
Compliance Integration
forgelm/compliance.py
generate_data_governance_report() refactored to inline training.output_dir/data_audit_report.json into the governance report under data_audit when present; read/parse errors warn and do not abort generation.
Wizard Directory Handling
forgelm/wizard.py
_validate_local_jsonl now detects directory inputs, returns None to re-prompt, and emits a forgelm ingest --recursive hint.
Package Configuration
forgelm/__init__.py, pyproject.toml
Version bumped to 0.5.0rc1; new [project.optional-dependencies].ingestion extra added for document parsing dependencies.
Tests — Ingestion
tests/test_ingestion.py
New comprehensive tests for supported formats, chunking strategies (sliding/paragraph), JSONL output schema, PII masking, recursion behavior, unsupported-extension skipping, and optional extractor cases guarded by dependency presence.
Tests — Data Audit
tests/test_data_audit.py
New comprehensive audit tests covering PII detection/masking (email, CC Luhn, TR ID, SSN rules), simhash determinism and Hamming thresholds, split discovery/aliasing, near-duplicate and cross-split leakage detection, schema drift tolerance, malformed JSONL resilience, and report serialization.
Tests — Wizard
tests/test_wizard_byod.py
Updates directory-rejection assertion to expect an explicit directory message and a forgelm ingest hint.
Compliance & Tests Integration
tests/test_compliance.py
Adds tests verifying governance inlining behavior for present/malformed/missing data_audit_report.json.
CI Workflow
.github/workflows/nightly.yml
Adds nightly workflow step performing a minimal end-to-end ingest → audit flow and asserting non-empty outputs.
Docs — Guides & Usage
docs/guides/ingestion.md, docs/guides/data_audit.md, docs/reference/usage.md, docs/guides/quickstart.md
Adds ingestion and audit guides and usage docs: CLI examples, chunking semantics, PII masking behavior, audit metrics/report structure, troubleshooting, end-to-end ingest→audit examples, and quickstart updates.
Docs — Roadmap / Changelog / README / Architecture
CHANGELOG.md, README.md, docs/roadmap*.md, docs/standards/architecture.md
Updates changelog and README for v0.5.0 Phase 11; marks Phase 11 complete in roadmap, adds Phase 11.5 backlog, updates architecture docs to list new modules and new optional extras.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as forgelm ingest<br/>(CLI)
    participant Extractor as Format Extractor<br/>(PDF/DOCX/EPUB/TXT)
    participant Chunker as Chunker<br/>(paragraph/sliding)
    participant PII as PII Masker
    participant Writer as JSONL Writer
    participant Output as /tmp/out.jsonl

    User->>CLI: forgelm ingest input/ --output /tmp/out.jsonl
    CLI->>Extractor: enumerate files (recursive?)
    Extractor->>Extractor: parse per-format text
    Extractor->>Chunker: send extracted text
    Chunker->>Chunker: split according to strategy
    Chunker->>PII: deliver chunks
    alt --pii-mask enabled
        PII->>PII: detect & redact sensitive matches
    end
    PII->>Writer: write {"text": "..."} lines
    Writer->>Output: persist JSONL
    Output-->>CLI: return IngestionResult
    CLI-->>User: summary + next-step hint
Loading
sequenceDiagram
    participant User
    participant CLI as forgelm --data-audit<br/>(CLI)
    participant Resolver as Split Resolver
    participant Reader as JSONL Reader
    participant Metrics as Metrics Engine
    participant Overlap as Cross-Split Detector
    participant Report as Report Writer
    participant OutputDir as output_dir/

    User->>CLI: forgelm --data-audit --input dataset.jsonl --output output_dir
    CLI->>Resolver: map input -> splits (train/validation/test)
    Resolver->>Reader: read JSONL lines per split
    Reader->>Metrics: extract text fields & compute stats
    Metrics->>Metrics: detect PII, compute simhash, near-duplicates, language
    Metrics->>Overlap: compare fingerprints across splits
    Overlap->>Report: record leakage & leaked counts
    Report->>OutputDir: write data_audit_report.json
    OutputDir-->>CLI: return AuditReport summary & path
    CLI-->>User: summary + report path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibble pages, parse each line,

Chunk by chunk the texts align,
Secrets masked and echoes found,
Audit JSONs stored and bound,
A hop from docs to training shine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: landing Phase 11 (Document Ingestion & Data Audit) and syncing post-v0.4.5 changes for v0.5.0 release.
Description check ✅ Passed The description thoroughly covers the Summary, detailed Changes with multiple subsections, Type selection (New feature + Documentation), comprehensive Testing checklist, and project Checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@codacy-production

codacy-production Bot commented Apr 26, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 415 complexity · 2 duplication

Metric Results
Complexity 415
Duplication 2

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 implements Phase 11 of the roadmap, introducing a document ingestion pipeline and a dataset audit tool. The ingestion module supports PDF, DOCX, EPUB, TXT, and Markdown formats with paragraph and sliding window chunking strategies, including optional PII masking. The audit tool provides metrics on sample counts, length distributions, language detection, near-duplicate detection via simhash, and cross-split leakage checks. Feedback focused on performance bottlenecks in the auditing logic due to quadratic complexity in duplicate detection and a discrepancy in DOCX extraction where table content was being ignored despite documentation claims.

Comment thread forgelm/data_audit.py
Comment on lines +255 to +278
def find_near_duplicates(
fingerprints: List[int],
*,
threshold: int = DEFAULT_NEAR_DUP_HAMMING,
) -> List[Tuple[int, int, int]]:
"""Pair-find rows whose simhash Hamming distance ≤ ``threshold``.

Returns ``[(i, j, distance), ...]`` with ``i < j``. Quadratic in the
number of fingerprints; intended for audit-time use on datasets up to
~50 K rows. For larger datasets, an LSH band index would be required —
out of scope for v0.5.0.
"""
pairs: List[Tuple[int, int, int]] = []
for i, fp_i in enumerate(fingerprints):
if fp_i == 0:
continue
for j in range(i + 1, len(fingerprints)):
fp_j = fingerprints[j]
if fp_j == 0:
continue
distance = hamming_distance(fp_i, fp_j)
if distance <= threshold:
pairs.append((i, j, distance))
return pairs

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 find_near_duplicates function implements a nested loop with O(n^2) complexity. For the documented target scale of ~50,000 rows, this results in approximately 1.25 billion iterations. In pure Python, this will be extremely slow and may take several minutes or even hours to complete. Additionally, if the dataset contains many near-duplicates, the pairs list can grow very large, potentially leading to high memory consumption (e.g., 10k identical rows would generate ~50 million tuples). Since the audit report only requires the count of near-duplicate pairs, consider using a more efficient approach like LSH-based bucketing (pigeonhole principle with blocks) and only returning the count to save memory.

Comment thread forgelm/data_audit.py Outdated
fp_b_set = {fp for fp in fingerprints_by_split[b] if fp != 0}
if not fp_b_set:
continue
shared = sum(1 for fp in fp_a if any(hamming_distance(fp, other) <= threshold for other in fp_b_set))

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

This cross-split overlap check also has O(N_a * N_b) complexity. If both splits are large (e.g., 50k rows each), this will perform up to 2.5 billion Hamming distance calculations in pure Python, which is likely to be a significant performance bottleneck for the CLI tool.

Comment thread forgelm/ingestion.py Outdated
Comment on lines +114 to +124
def _extract_docx(path: Path) -> str:
try:
from docx import Document
except ImportError as exc: # pragma: no cover
raise ImportError(
"DOCX ingestion requires the 'ingestion' extra. Install with: pip install 'forgelm[ingestion]'"
) from exc

doc = Document(str(path))
paragraphs = [p.text for p in doc.paragraphs if p.text and p.text.strip()]
return "\n\n".join(paragraphs)

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 _extract_docx function currently only extracts text from doc.paragraphs, which excludes content within tables. This contradicts the documentation in docs/guides/ingestion.md (line 183) which states that DOCX tables are flattened to plain text. To align with the documentation and ensure comprehensive data ingestion, table content should be extracted as well.

def _extract_docx(path: Path) -> str:
    try:
        from docx import Document
    except ImportError as exc:  # pragma: no cover
        raise ImportError(
            "DOCX ingestion requires the 'ingestion' extra. Install with: pip install 'forgelm[ingestion]'"
        ) from exc

    doc = Document(str(path))
    text_parts = [p.text for p in doc.paragraphs if p.text and p.text.strip()]
    # Also extract text from tables to align with documentation
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                if cell.text and cell.text.strip():
                    text_parts.append(cell.text)
    return "\n\n".join(text_parts)

@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: 12

Caution

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

⚠️ Outside diff range comments (2)
docs/standards/architecture.md (1)

71-71: ⚠️ Potential issue | 🟡 Minor

Stale module count.

The "Seventeen single-file modules" claim is now off — the table at lines 79-96 has 18 rows after ingestion.py and data_audit.py were added (and arguably inference.py from Phase 10 is also unaccounted for). Bump the prose so it stays in step with the topology table.

📝 Suggested fix
-Seventeen single-file modules. **No sub-packages inside `forgelm/`.** If a module grows past ~1000 lines and has cohesive subsections, split into `module_name/` package, but keep the public API at `forgelm.module_name.X` so imports don't break.
+Eighteen single-file modules. **No sub-packages inside `forgelm/`.** If a module grows past ~1000 lines and has cohesive subsections, split into `module_name/` package, but keep the public API at `forgelm.module_name.X` so imports don't break.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/standards/architecture.md` at line 71, Update the stale sentence
"Seventeen single-file modules." to accurately reflect the current module count
(at least 18) or, better, remove the hard-coded number and replace it with a
dynamic phrasing like "multiple single-file modules" or "N single-file modules"
that is maintained alongside the topology table; change the exact text token
"Seventeen single-file modules." in the architecture prose so it matches the
modules listed (including ingestion.py, data_audit.py, and inference.py) and
ensure the guidance about splitting modules into module_name/ packages (and
keeping public API at forgelm.module_name.X) remains unchanged.
README.md (1)

241-241: ⚠️ Potential issue | 🟡 Minor

Stale test count in Project Structure block.

The PR description states 718 passed, 11 skipped, but this line still advertises # 430 passed (+34 skipped) across 30 test files. Either drop the count (it goes stale every PR) or update it to match v0.5.0rc1.

📝 Proposed fix
-tests/               # 430 passed (+34 skipped) across 30 test files
+tests/               # Unit + integration suite (run `pytest tests/`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 241, The "tests/" line in README.md contains a stale test
summary ("# 430 passed (+34 skipped) across 30 test files") that doesn't match
the PR/test run (718 passed, 11 skipped) — update or remove this count to avoid
future rot: either replace the existing summary text next to the "tests/" entry
with the current v0.5.0rc1 results (718 passed, 11 skipped) or remove the
numeric summary entirely so the "tests/" bullet remains without hard-coded
counts; edit the line that begins with "tests/" in README.md to apply the
change.
♻️ Duplicate comments (1)
docs/roadmap-tr.md (1)

20-20: ⚠️ Potential issue | 🟡 Minor

Same v0.4.0 date drift as roadmap.md.

CHANGELOG.md line 105 has v0.4.0 = 2026-04-25, not 2026-04-26. Fix here to keep the EN/TR mirror in sync once docs/roadmap.md is corrected.

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

In `@docs/roadmap-tr.md` at line 20, The v0.4.0 entry in docs/roadmap-tr.md
currently shows the date 2026-04-26 which drifts from CHANGELOG.md and the EN
roadmap; update the date string for the `v0.4.0` entry to 2026-04-25 so the
Turkish mirror (`v0.4.0` — Post-Training Tamamlama) matches the canonical
sources.
🧹 Nitpick comments (13)
forgelm/compliance.py (1)

139-151: Article 10 inlining is well-scoped.

The defensive getattr chain, file existence check, and explicit catch of JSONDecodeError/OSError keep this best-effort and non-fatal. One small consideration: a corrupt UTF-8 file would raise UnicodeDecodeError (not in the catch tuple) and bubble up. If you want full robustness, broaden to (json.JSONDecodeError, OSError, UnicodeDecodeError) so a damaged audit artifact never crashes the governance report.

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

In `@forgelm/compliance.py` around lines 139 - 151, The current inlining of
data_audit_report.json uses a try/except that catches json.JSONDecodeError and
OSError but will still raise on a corrupt UTF-8 file (UnicodeDecodeError);
update the exception tuple in the except block that surrounds the open+json.load
(the block that assigns report["data_audit"]) to include UnicodeDecodeError so
the handler (logger.warning(..., audit_path, exc)) swallows encoding errors as
well and keeps the process non‑fatal for output_dir/audit_path.
forgelm/cli.py (2)

1106-1117: Exit-code choice for missing extras differs from sibling commands.

_run_chat_cmd (line 789–791) maps ImportError to EXIT_TRAINING_ERROR (2), but here the missing-extras case maps to EXIT_CONFIG_ERROR (1). Both are valid public-contract codes, but the inconsistency makes it harder for CI scripts to branch on "missing optional dep". Pick one convention across subcommands and document it in docs/standards/error-handling.md if it isn't already.

As per coding guidelines: "Exit codes are a public contract: 0/1/2/3/4 as per error-handling.md, especially for CLI".

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

In `@forgelm/cli.py` around lines 1106 - 1117, The ImportError handler in the
ingest command currently exits with EXIT_CONFIG_ERROR while _run_chat_cmd maps
ImportError to EXIT_TRAINING_ERROR; make the exit-code convention consistent by
changing the ImportError except block (the "except ImportError as exc:" handler
that uses output_format and logger) to exit with EXIT_TRAINING_ERROR instead of
EXIT_CONFIG_ERROR, and update docs/standards/error-handling.md to document that
missing optional dependency (ImportError) should use EXIT_TRAINING_ERROR across
subcommands.

1139-1153: _run_data_audit only catches FileNotFoundError.

audit_dataset calls out_dir.mkdir(...) and open(out_path, "w", ...), both of which can raise OSError (read-only FS, permission denied, ENOSPC). _resolve_input and _read_jsonl_split can also surface non-FileNotFoundError OSError cases. Today those will produce a Python traceback instead of a clean JSON/text error envelope.

♻️ Suggested broaden
-    try:
-        report = audit_dataset(audit_input, output_dir=target)
-    except FileNotFoundError as exc:
+    try:
+        report = audit_dataset(audit_input, output_dir=target)
+    except (FileNotFoundError, OSError, ValueError) as exc:
         if output_format == "json":
             print(json.dumps({"success": False, "error": str(exc)}))
         else:
             logger.error("Audit failed: %s", exc)
         sys.exit(EXIT_CONFIG_ERROR)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/cli.py` around lines 1139 - 1153, The current _run_data_audit
function only catches FileNotFoundError from audit_dataset, leaving other
filesystem-related OSError subclasses (permission errors, ENOSPC, etc.)
unhandled; update the except clause to catch OSError (or add an additional
except OSError as exc) so any filesystem/IO errors from audit_dataset,
_resolve_input, or _read_jsonl_split are handled the same way: when
output_format == "json" print a JSON error envelope with the stringified exc,
otherwise call logger.error("Audit failed: %s", exc), and then
sys.exit(EXIT_CONFIG_ERROR) — reference _run_data_audit, audit_dataset,
output_format, logger, json and EXIT_CONFIG_ERROR to locate the change.
forgelm/data_audit.py (4)

100-100: Use a greedy quantifier for the credit-card pattern.

\b(?:\d[ -]*?){13,19}\b works (regex backtracking will eventually consume separators when the surrounding \b forces it), but the reluctant *? adds avoidable backtracking on long digit-rich text and is what SonarCloud is flagging. A greedy * is functionally equivalent here and faster.

♻️ Proposed change
-    "credit_card": re.compile(r"\b(?:\d[ -]*?){13,19}\b"),
+    "credit_card": re.compile(r"\b(?:\d[ -]*){13,19}\b"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/data_audit.py` at line 100, The credit card regex under "credit_card"
uses a reluctant quantifier (\d[ -]*?) which causes unnecessary backtracking;
update the pattern in forgelm/data_audit.py to use a greedy quantifier (\d[ -]*)
so the entry "credit_card" becomes \b(?:\d[ -]*){13,19}\b (keep the surrounding
word boundaries and same repetition range) to improve performance while
preserving behavior.

459-514: _resolve_input complexity warning is fixable by extracting the directory branch.

SonarCloud flags 19 vs. 15. Splitting the directory-scanning logic into _resolve_directory_splits(src) -> (layouts, notes) (returning ({}, [...]) when no canonical files match so the pseudo-split fallback can take over) would cut the nesting and let each branch be tested in isolation.

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

In `@forgelm/data_audit.py` around lines 459 - 514, The _resolve_input function is
too complex; extract the directory-scanning branch into a new helper
_resolve_directory_splits(src) that encapsulates the logic currently inside the
if src.is_dir() block (use _SPLIT_ALIASES, discovery of canonical/alias files,
building layouts, notes, the pseudo-split fallback and logger.warning), and have
_resolve_input call this helper and return its (layouts, notes) result or
continue to raise FileNotFoundError for missing input; this reduces nesting and
allows unit testing of directory logic in _resolve_directory_splits separately.

435-438: Mixed .format() inside a dict literal is easy to misread.

"leaked_rows_in_{}".format(a) works but the surrounding code uses f-strings everywhere; this is the only positional .format call in the file. Also, the if fp_a else 0.0 branch on line 437 is unreachable — the function continues on if not fp_a at line 428.

♻️ Proposed cleanup
             report["pairs"][f"{a}__{b}"] = {
-                "leaked_rows_in_{}".format(a): shared,
-                "leak_rate": round(shared / len(fp_a), 4) if fp_a else 0.0,
+                f"leaked_rows_in_{a}": shared,
+                "leak_rate": round(shared / len(fp_a), 4),
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/data_audit.py` around lines 435 - 438, The dict entry creation in
report["pairs"][f"{a}__{b}"] mixes a .format() call with f-strings and includes
an unreachable ternary because fp_a is already guarded by an earlier continue;
change the key name to use an f-string (e.g., f"leaked_rows_in_{a}") for
consistency and remove the unnecessary "if fp_a else 0.0" branch so leak_rate is
computed as round(shared / len(fp_a), 4) (relying on the prior if not fp_a:
continue), updating the code that builds report["pairs"][f"{a}__{b}"]
accordingly.

352-415: _audit_split is doing five things; pulling out the per-feature passes would tame the complexity.

SonarCloud flags cognitive complexity 25 vs. 15 budget. The function currently mixes schema/drift, text payload extraction, length stats, language sampling, simhash fingerprinting, and PII counting. Each pass is independent and easy to extract:

  • _compute_schema(rows) -> (columns, drift_columns)
  • _compute_payloads(rows) -> (payloads, null_or_empty_count)
  • _compute_languages(payloads, sample_size=200)
  • _compute_fingerprints(split_name, payloads)
  • _compute_pii(payloads)

That keeps the public per-split orchestrator readable and gives each helper an independently testable surface.

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

In `@forgelm/data_audit.py` around lines 352 - 415, _audit_split currently exceeds
cognitive complexity by mixing schema/drift discovery, payload extraction,
length stats, language sampling, simhashing and PII counting into one large
function; refactor by extracting those independent passes into small helpers and
call them from _audit_split: implement _compute_schema(rows) -> (columns,
drift_columns) to build seen_columns and detect drift vs base_columns,
_compute_payloads(rows) -> (text_payloads, null_or_empty) to run
_extract_text_payload and compute null counts, _compute_languages(text_payloads,
sample_size=200) to sample and return top language counts,
_compute_fingerprints(split_name, text_payloads) to compute simhashes with
progress logging and return fingerprints and simhash distinct count, and
_compute_pii(text_payloads) to aggregate detect_pii results; update _audit_split
to orchestrate these helpers and assemble info, fingerprints and
pii_counts_split accordingly so behavior and returned tuple remain unchanged.
forgelm/ingestion.py (1)

291-414: Consider extracting the per-file processing loop.

SonarCloud flags cognitive complexity 30 (vs. 15 budget) for ingest_path. The body is dominated by the per-file loop (extract → handle errors → chunk → optional PII mask → write). Pulling that into a _process_file(...) helper that returns chunk-count / chars / pii-counts would bring the public function back under budget and make the test surface narrower.

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

In `@forgelm/ingestion.py` around lines 291 - 414, ingest_path is over-complex due
to a large per-file loop; extract that logic into a new helper (e.g.
_process_file) that takes (fpath, strategy, chunk_size, overlap, mask_pii,
detect_pii, out_fh) and returns a small result struct/tuple (chunks_written,
chars_written, per_file_pii_counts, skipped_flag, format_ext) so the main
ingest_path just iterates files, calls _select_extractor then _process_file, and
accumulates chunk_count, total_chars, format_counts and pii_redaction_counts;
move error handling for extractor calls, empty-text checks, chunking via
_strategy_dispatch, optional PII detect_pii/mask_pii logic, and the
out_fh.write(json.dumps(...)) into _process_file to reduce ingest_path cognitive
complexity while preserving existing symbols (ingest_path, _select_extractor,
_strategy_dispatch, detect_pii, mask_pii, IngestionResult).
pyproject.toml (1)

86-92: Pin upper bounds on the new ingestion extras.

The other optional groups consistently cap major versions (e.g. bitsandbytes>=0.42.0,<1.0.0, lm-eval>=0.4.4,<1.0.0). The new group uses open-ended >= only, which means a future pypdf 6.x or python-docx 2.x with breaking API changes would be silently pulled in by pip install 'forgelm[ingestion]' and break the format extractors at runtime. (pypdf 6.x is already publicly available as of April 2026.)

♻️ Proposed bounds
 ingestion = [
-  "pypdf>=4.0.0",
-  "python-docx>=1.0.0",
-  "ebooklib>=0.18",
-  "beautifulsoup4>=4.12.0",
-  "langdetect>=1.0.9",
+  "pypdf>=4.0.0,<6.0.0",
+  "python-docx>=1.0.0,<2.0.0",
+  "ebooklib>=0.18,<1.0.0",
+  "beautifulsoup4>=4.12.0,<5.0.0",
+  "langdetect>=1.0.9,<2.0.0",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 86 - 92, The ingestion extras in pyproject.toml
currently use open-ended >= pins for pypdf, python-docx, ebooklib,
beautifulsoup4, and langdetect; update the ingestion array to add conservative
upper bounds that cap the next major (e.g., pypdf>=4.0.0,<6.0.0,
python-docx>=1.0.0,<2.0.0, ebooklib>=0.18,<1.0.0, beautifulsoup4>=4.12.0,<5.0.0,
langdetect>=1.0.9,<2.0.0) so future breaking major releases won’t be pulled
in—modify the ingestion extras entry in pyproject.toml accordingly.
docs/roadmap/phase-11-data-ingestion.md (1)

3-3: Status banner is fine but the static-analysis hint is worth a glance.

markdownlint flags MD028 (blank line inside the blockquote that follows on line 5) and MD001 (heading jumps from # to ### Tasks: at line 13). Neither blocks anything, but if you're already touching the file you can collapse the blank line and demote ### Tasks: to ## Tasks: to keep the linter quiet.

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

In `@docs/roadmap/phase-11-data-ingestion.md` at line 3, The markdown linter
warnings come from a blank line inside the blockquote after the Status banner
and an incorrectly leveled heading; remove the empty line inside the blockquote
(collapse the blockquote so there is no blank line after the Status line) and
change the heading "### Tasks:" to "## Tasks:" so the heading levels do not jump
and MD028/MD001 are resolved.
tests/test_data_audit.py (3)

76-78: Silence SonarCloud type-arg warnings consistently with line 123.

These two calls intentionally exercise the non-string guard in detect_pii, but SonarCloud flags them (failures at L77 and L78). Line 123 already uses # type: ignore[arg-type] for the same scenario on mask_pii — apply the same suppression here for consistency and a clean CI run.

♻️ Proposed change
     def test_returns_empty_for_non_string(self):
-        assert detect_pii(None) == {}
-        assert detect_pii(42) == {}
+        assert detect_pii(None) == {}  # type: ignore[arg-type]
+        assert detect_pii(42) == {}  # type: ignore[arg-type]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_data_audit.py` around lines 76 - 78, Add the same SonarCloud
type-arg suppression used for mask_pii to these tests: in
test_returns_empty_for_non_string update the two assertions that call
detect_pii(None) and detect_pii(42) to append a "# type: ignore[arg-type]"
comment to each call so the non-string guard is still tested but static type
checkers/CI warnings are suppressed; locate the assertions inside the
test_returns_empty_for_non_string function around detect_pii and add the
comments there.

109-116: Drop the redundant local import of _is_credit_card.

_is_credit_card is already imported at the top of the file (line 17), so the inline from forgelm.data_audit import _is_credit_card here is dead. The explanatory docstring above is fine to keep.

♻️ Proposed change
     def test_luhn_helper_distinguishes_valid_from_invalid(self):
         # The masker may also redact long digit runs as candidate phone
         # numbers (false positives are intentional per the module docstring).
         # We assert at the helper level instead, which is unambiguous.
-        from forgelm.data_audit import _is_credit_card
-
         assert _is_credit_card("4111111111111111") is True
         assert _is_credit_card("4111111111111112") is False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_data_audit.py` around lines 109 - 116, Remove the redundant inline
import in the test method test_luhn_helper_distinguishes_valid_from_invalid:
delete the line "from forgelm.data_audit import _is_credit_card" and rely on the
existing module-level import of _is_credit_card; leave the surrounding docstring
and the two assert lines unchanged so the test still calls the helper
_is_credit_card("4111111111111111") and _is_credit_card("4111111111111112").

257-266: caplog.at_level("WARNING") context is set up but never asserted on.

The PR comment for Bug 26 says the operator must be warned about pseudo-splits, but this test only verifies the report.notes blob — nothing checks that a WARNING-level log record was actually emitted. Either assert on caplog.records (the stronger guarantee, since notes and logs can drift apart) or drop the caplog fixture entirely to keep the test honest.

♻️ Proposed change (assert the warning was logged)
-    def test_pseudo_split_fallback_warns(self, tmp_path, caplog):
+    def test_pseudo_split_fallback_warns(self, tmp_path, caplog):
         # Bug 26: when no canonical split files exist, every .jsonl becomes
         # its own pseudo-split BUT the operator must be warned that
         # cross-split leakage analysis is meaningless in that case.
         _write_jsonl(tmp_path / "alpha.jsonl", [{"text": "x"}])
         _write_jsonl(tmp_path / "beta.jsonl", [{"text": "y"}])
         with caplog.at_level("WARNING"):
             report = audit_dataset(str(tmp_path))
         assert "alpha" in report.splits and "beta" in report.splits
         assert any("pseudo-split" in n for n in report.notes)
+        assert any(
+            r.levelname == "WARNING" and "pseudo-split" in r.getMessage()
+            for r in caplog.records
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_data_audit.py` around lines 257 - 266, The test
test_pseudo_split_fallback_warns sets up caplog but never asserts a log was
emitted; update the test to assert that a WARNING-level log record about
pseudo-splits was emitted when calling audit_dataset (e.g., inspect
caplog.records or caplog.text for a message containing "pseudo-split" or
similar) in addition to the existing report.notes checks, or remove the caplog
usage if you prefer not to assert logs; focus your change inside
test_pseudo_split_fallback_warns and reference caplog, audit_dataset, and
report.notes when adding the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guides/data_audit.md`:
- Line 58: The fenced code block starting before the JSON object that contains
"cross_split_overlap" is missing a language identifier; update the opening
triple-backtick fence to include "json" so markdownlint MD040 is satisfied
(i.e., change ``` to ```json immediately before the object that begins with
"cross_split_overlap"). Ensure the rest of the block remains unchanged and that
the closing ``` remains present.
- Around line 21-24: The documentation currently contradicts itself about what
`--output-format json` prints; reconcile both descriptions so they match: decide
whether `--output-format json` writes the full JSON report to stdout or only a
summary (while always writing the full `data_audit_report.json` to `--output`),
then update the earlier paragraph (mentions `--output`,
`data_audit_report.json`, and `--output-format json`) and the later Note (the
explicit Note that references `--output-format json`, top-level metrics + report
path, and `data_audit_report.json`) to state the chosen behavior consistently
and clearly for CI users.
- Around line 1-7: Create a Turkish mirror of the user-facing guide by adding a
new file named data_audit-tr.md that is a full translation of data_audit.md:
translate the title and body into Turkish but preserve and not translate
code/CLI examples such as `forgelm --data-audit` and artifact names like
`data_audit_report.json`, keep references to trainer concepts like `output_dir`,
and ensure frontmatter/metadata (if any) matches the original; finally, register
the new Turkish page in the docs navigation/sidebar so it appears alongside the
English guide.

In `@docs/guides/ingestion.md`:
- Around line 116-122: The docs currently state encrypted PDFs surface a
ValueError to the operator, but the actual behavior in PdfExtractor and tests
(see TestPdfExtractor::test_encrypted_pdf_raises_clear_error) is to catch the
failure per-file, log a warning, and append the file to files_skipped rather
than exiting; update the wording in docs/guides/ingestion.md to describe this
per-file handling (warning + files_skipped) and include the suggested
external-decrypt command as remediation for skipped files.
- Around line 1-10: Add a Turkish mirror of the user-facing guide by creating a
new file named docs/guides/ingestion-tr.md that is a bilingual mirror of
docs/guides/ingestion.md: copy the content and translate all user-facing text
into Turkish, preserve any Markdown structure, links (adjust localized link
targets if needed), headings, code blocks and metadata, and ensure the new file
matches the same frontmatter/formatting conventions as other -tr guides so it
appears alongside ingestion.md in the docs build.

In `@docs/reference/usage.md`:
- Around line 165-168: The closing trailer sentence "This uses the `synthetic`
config section to generate training data from a teacher model before training
begins. See the [Configuration Guide](configuration.md) for all synthetic data
options." is orphaned and must be moved so it immediately follows the Synthetic
Data Generation section (the paragraph that begins with the forgelm --config
example), not after the Document Ingestion / Dataset Audit blocks; relocate that
exact sentence to sit directly after the synthetic example/trailer in the
Synthetic Data Generation section (or hoist it up before inserting the Document
Ingestion and Dataset Audit sections) so the Document Ingestion (v0.5.0+) and
Dataset Audit paragraphs appear after that trailer and read correctly.
- Around line 130-166: Add Turkish translations of the two missing sections
titled "Document Ingestion (v0.5.0+)" and "Dataset Audit (v0.5.0+)" into the
Turkish mirror (usage-tr) so it matches the English usage reference; include the
same command examples (single file, recursive, sliding-window, PII mask)
translated text for descriptions, keep the version note "(v0.5.0+)", preserve
links to the corresponding guides (ingestion and data_audit) and the note about
the trainer inlining data_audit findings under the data_audit key so the
bilingual docs remain in sync.

In `@docs/roadmap.md`:
- Line 20: Update the date for the v0.4.0 roadmap entry where the string "v0.4.0
— Post-Training Completion (2026-04-26)" appears so it reads "v0.4.0 —
Post-Training Completion (2026-04-25)" to match CHANGELOG.md; make the identical
replacement in the translated roadmap entry that contains the same v0.4.0
string.

In `@docs/roadmap/phase-11-5-backlog.md`:
- Around line 11-12: The count in the table header (| # | Item | Why deferred |
Effort | Impact |) and the PR summary disagree: update the table rows or the PR
prose so they match; either add the two missing rows to the table to reach "14
deferred follow-ups" or change the PR summary to "12 deferred follow-ups", and
if two items indeed landed mid-cycle, list them under the "Recently landed"
section (mention their titles/IDs) so the document and PR description are
consistent.

In `@docs/roadmap/phase-11-data-ingestion.md`:
- Around line 15-16: Update the roadmap entry to match the shipped ingestion
output: replace the SFT-style {"messages":[...]} schema with the actual JSONL
record shape emitted by forgelm.ingestion (records of the form {"text":
"<chunk>"}), keep the same mention of supported input formats and chunking
strategies (sliding/paragraph/semantic), and note the v0.5.0 behavior and
optional dependency group (forgelm[ingestion]); also update related docs
(releases and reference/usage) to use the same {"text": ...} schema so loaders
aren’t written against a nonexistent messages format.

In `@forgelm/ingestion.py`:
- Around line 374-380: The current code increments pii_redaction_counts by
running detect_pii(payload) which independently applies all _PII_PATTERNS and
can double-count spans that match multiple patterns while mask_pii mutates the
text in a single pass, causing counts to drift; change mask_pii to perform the
single-pass redaction and also return a per-type count (e.g., return
(masked_text, counts_dict)), then replace the detect_pii(payload) usage in this
block with the counts returned from mask_pii so pii_redaction_counts is
incremented from the actual redactions, and update any call sites of mask_pii to
accept the new return shape (or alternatively make mask_pii accept a flag to
return counts) while preserving existing behavior for other callers.

In `@tests/test_ingestion.py`:
- Around line 257-274: Update the docs text in guides/ingestion.md so it matches
the actual behavior asserted by test_encrypted_pdf_raises_clear_error: instead
of saying a ValueError is raised, state that encrypted PDFs are logged as a
warning and skipped during batch ingestion (files_skipped increments) and ensure
the warning message recommends the external decrypt path to the operator;
reference the ingest_path flow and the test name to locate the behavior to
document.

---

Outside diff comments:
In `@docs/standards/architecture.md`:
- Line 71: Update the stale sentence "Seventeen single-file modules." to
accurately reflect the current module count (at least 18) or, better, remove the
hard-coded number and replace it with a dynamic phrasing like "multiple
single-file modules" or "N single-file modules" that is maintained alongside the
topology table; change the exact text token "Seventeen single-file modules." in
the architecture prose so it matches the modules listed (including ingestion.py,
data_audit.py, and inference.py) and ensure the guidance about splitting modules
into module_name/ packages (and keeping public API at forgelm.module_name.X)
remains unchanged.

In `@README.md`:
- Line 241: The "tests/" line in README.md contains a stale test summary ("# 430
passed (+34 skipped) across 30 test files") that doesn't match the PR/test run
(718 passed, 11 skipped) — update or remove this count to avoid future rot:
either replace the existing summary text next to the "tests/" entry with the
current v0.5.0rc1 results (718 passed, 11 skipped) or remove the numeric summary
entirely so the "tests/" bullet remains without hard-coded counts; edit the line
that begins with "tests/" in README.md to apply the change.

---

Duplicate comments:
In `@docs/roadmap-tr.md`:
- Line 20: The v0.4.0 entry in docs/roadmap-tr.md currently shows the date
2026-04-26 which drifts from CHANGELOG.md and the EN roadmap; update the date
string for the `v0.4.0` entry to 2026-04-25 so the Turkish mirror (`v0.4.0` —
Post-Training Tamamlama) matches the canonical sources.

---

Nitpick comments:
In `@docs/roadmap/phase-11-data-ingestion.md`:
- Line 3: The markdown linter warnings come from a blank line inside the
blockquote after the Status banner and an incorrectly leveled heading; remove
the empty line inside the blockquote (collapse the blockquote so there is no
blank line after the Status line) and change the heading "### Tasks:" to "##
Tasks:" so the heading levels do not jump and MD028/MD001 are resolved.

In `@forgelm/cli.py`:
- Around line 1106-1117: The ImportError handler in the ingest command currently
exits with EXIT_CONFIG_ERROR while _run_chat_cmd maps ImportError to
EXIT_TRAINING_ERROR; make the exit-code convention consistent by changing the
ImportError except block (the "except ImportError as exc:" handler that uses
output_format and logger) to exit with EXIT_TRAINING_ERROR instead of
EXIT_CONFIG_ERROR, and update docs/standards/error-handling.md to document that
missing optional dependency (ImportError) should use EXIT_TRAINING_ERROR across
subcommands.
- Around line 1139-1153: The current _run_data_audit function only catches
FileNotFoundError from audit_dataset, leaving other filesystem-related OSError
subclasses (permission errors, ENOSPC, etc.) unhandled; update the except clause
to catch OSError (or add an additional except OSError as exc) so any
filesystem/IO errors from audit_dataset, _resolve_input, or _read_jsonl_split
are handled the same way: when output_format == "json" print a JSON error
envelope with the stringified exc, otherwise call logger.error("Audit failed:
%s", exc), and then sys.exit(EXIT_CONFIG_ERROR) — reference _run_data_audit,
audit_dataset, output_format, logger, json and EXIT_CONFIG_ERROR to locate the
change.

In `@forgelm/compliance.py`:
- Around line 139-151: The current inlining of data_audit_report.json uses a
try/except that catches json.JSONDecodeError and OSError but will still raise on
a corrupt UTF-8 file (UnicodeDecodeError); update the exception tuple in the
except block that surrounds the open+json.load (the block that assigns
report["data_audit"]) to include UnicodeDecodeError so the handler
(logger.warning(..., audit_path, exc)) swallows encoding errors as well and
keeps the process non‑fatal for output_dir/audit_path.

In `@forgelm/data_audit.py`:
- Line 100: The credit card regex under "credit_card" uses a reluctant
quantifier (\d[ -]*?) which causes unnecessary backtracking; update the pattern
in forgelm/data_audit.py to use a greedy quantifier (\d[ -]*) so the entry
"credit_card" becomes \b(?:\d[ -]*){13,19}\b (keep the surrounding word
boundaries and same repetition range) to improve performance while preserving
behavior.
- Around line 459-514: The _resolve_input function is too complex; extract the
directory-scanning branch into a new helper _resolve_directory_splits(src) that
encapsulates the logic currently inside the if src.is_dir() block (use
_SPLIT_ALIASES, discovery of canonical/alias files, building layouts, notes, the
pseudo-split fallback and logger.warning), and have _resolve_input call this
helper and return its (layouts, notes) result or continue to raise
FileNotFoundError for missing input; this reduces nesting and allows unit
testing of directory logic in _resolve_directory_splits separately.
- Around line 435-438: The dict entry creation in report["pairs"][f"{a}__{b}"]
mixes a .format() call with f-strings and includes an unreachable ternary
because fp_a is already guarded by an earlier continue; change the key name to
use an f-string (e.g., f"leaked_rows_in_{a}") for consistency and remove the
unnecessary "if fp_a else 0.0" branch so leak_rate is computed as round(shared /
len(fp_a), 4) (relying on the prior if not fp_a: continue), updating the code
that builds report["pairs"][f"{a}__{b}"] accordingly.
- Around line 352-415: _audit_split currently exceeds cognitive complexity by
mixing schema/drift discovery, payload extraction, length stats, language
sampling, simhashing and PII counting into one large function; refactor by
extracting those independent passes into small helpers and call them from
_audit_split: implement _compute_schema(rows) -> (columns, drift_columns) to
build seen_columns and detect drift vs base_columns, _compute_payloads(rows) ->
(text_payloads, null_or_empty) to run _extract_text_payload and compute null
counts, _compute_languages(text_payloads, sample_size=200) to sample and return
top language counts, _compute_fingerprints(split_name, text_payloads) to compute
simhashes with progress logging and return fingerprints and simhash distinct
count, and _compute_pii(text_payloads) to aggregate detect_pii results; update
_audit_split to orchestrate these helpers and assemble info, fingerprints and
pii_counts_split accordingly so behavior and returned tuple remain unchanged.

In `@forgelm/ingestion.py`:
- Around line 291-414: ingest_path is over-complex due to a large per-file loop;
extract that logic into a new helper (e.g. _process_file) that takes (fpath,
strategy, chunk_size, overlap, mask_pii, detect_pii, out_fh) and returns a small
result struct/tuple (chunks_written, chars_written, per_file_pii_counts,
skipped_flag, format_ext) so the main ingest_path just iterates files, calls
_select_extractor then _process_file, and accumulates chunk_count, total_chars,
format_counts and pii_redaction_counts; move error handling for extractor calls,
empty-text checks, chunking via _strategy_dispatch, optional PII
detect_pii/mask_pii logic, and the out_fh.write(json.dumps(...)) into
_process_file to reduce ingest_path cognitive complexity while preserving
existing symbols (ingest_path, _select_extractor, _strategy_dispatch,
detect_pii, mask_pii, IngestionResult).

In `@pyproject.toml`:
- Around line 86-92: The ingestion extras in pyproject.toml currently use
open-ended >= pins for pypdf, python-docx, ebooklib, beautifulsoup4, and
langdetect; update the ingestion array to add conservative upper bounds that cap
the next major (e.g., pypdf>=4.0.0,<6.0.0, python-docx>=1.0.0,<2.0.0,
ebooklib>=0.18,<1.0.0, beautifulsoup4>=4.12.0,<5.0.0, langdetect>=1.0.9,<2.0.0)
so future breaking major releases won’t be pulled in—modify the ingestion extras
entry in pyproject.toml accordingly.

In `@tests/test_data_audit.py`:
- Around line 76-78: Add the same SonarCloud type-arg suppression used for
mask_pii to these tests: in test_returns_empty_for_non_string update the two
assertions that call detect_pii(None) and detect_pii(42) to append a "# type:
ignore[arg-type]" comment to each call so the non-string guard is still tested
but static type checkers/CI warnings are suppressed; locate the assertions
inside the test_returns_empty_for_non_string function around detect_pii and add
the comments there.
- Around line 109-116: Remove the redundant inline import in the test method
test_luhn_helper_distinguishes_valid_from_invalid: delete the line "from
forgelm.data_audit import _is_credit_card" and rely on the existing module-level
import of _is_credit_card; leave the surrounding docstring and the two assert
lines unchanged so the test still calls the helper
_is_credit_card("4111111111111111") and _is_credit_card("4111111111111112").
- Around line 257-266: The test test_pseudo_split_fallback_warns sets up caplog
but never asserts a log was emitted; update the test to assert that a
WARNING-level log record about pseudo-splits was emitted when calling
audit_dataset (e.g., inspect caplog.records or caplog.text for a message
containing "pseudo-split" or similar) in addition to the existing report.notes
checks, or remove the caplog usage if you prefer not to assert logs; focus your
change inside test_pseudo_split_fallback_warns and reference caplog,
audit_dataset, and report.notes when adding the assertion.
🪄 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: 51515584-5e3c-4707-8459-61905d832f97

📥 Commits

Reviewing files that changed from the base of the PR and between 45713dc and 0889fd6.

📒 Files selected for processing (25)
  • .github/workflows/nightly.yml
  • CHANGELOG.md
  • README.md
  • docs/guides/data_audit.md
  • docs/guides/ingestion.md
  • docs/guides/quickstart.md
  • docs/qms/sop_data_management.md
  • docs/reference/usage.md
  • docs/roadmap-tr.md
  • docs/roadmap.md
  • docs/roadmap/phase-11-5-backlog.md
  • docs/roadmap/phase-11-data-ingestion.md
  • docs/roadmap/phase-12-quickstart.md
  • docs/roadmap/releases.md
  • docs/standards/architecture.md
  • forgelm/__init__.py
  • forgelm/cli.py
  • forgelm/compliance.py
  • forgelm/data_audit.py
  • forgelm/ingestion.py
  • forgelm/wizard.py
  • pyproject.toml
  • tests/test_data_audit.py
  • tests/test_ingestion.py
  • tests/test_wizard_byod.py

Comment thread docs/guides/data_audit.md
Comment thread docs/guides/data_audit.md Outdated
Comment thread docs/guides/data_audit.md

### Cross-split overlap

```json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tag fenced block language for markdownlint MD040.

The fenced block at line 58 has no language identifier — markdownlint flags it. Use json to match the rest of the file.

📝 Proposed fix
-```
+```json
 {
   "cross_split_overlap": {
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 58-58: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@docs/guides/data_audit.md` at line 58, The fenced code block starting before
the JSON object that contains "cross_split_overlap" is missing a language
identifier; update the opening triple-backtick fence to include "json" so
markdownlint MD040 is satisfied (i.e., change ``` to ```json immediately before
the object that begins with "cross_split_overlap"). Ensure the rest of the block
remains unchanged and that the closing ``` remains present.

Comment thread docs/guides/ingestion.md
Comment on lines +1 to +10
# Document Ingestion Guide

Convert raw enterprise corpora (PDF / DOCX / EPUB / TXT / Markdown) into the
SFT-ready JSONL ForgeLM trains on. Phase 11; introduced in `v0.5.0`.

> Pair with [`forgelm --data-audit`](data_audit.md) afterwards to surface
> length-distribution / language / near-duplicate / PII metrics, and with
> [`forgelm --generate-data`](../reference/usage.md) if you want the chunks
> expanded into Q&A `messages` form via a teacher model.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'ingestion.*\.md$' docs/

Repository: cemililik/ForgeLM

Length of output: 126


🏁 Script executed:

#!/bin/bash
# List all markdown files in docs/guides/
echo "=== All markdown files in docs/guides/ ==="
fd -t f '\.md$' docs/guides/

echo ""
echo "=== Check for Turkish mirror patterns (-tr.md) ==="
fd -t f '\-tr\.md$' docs/guides/

echo ""
echo "=== Check entire docs directory for Turkish mirrors ==="
fd -t f '\-tr\.md$' docs/

Repository: cemililik/ForgeLM

Length of output: 668


Missing Turkish mirror for user-facing guide.

docs/guides/ingestion.md is missing a docs/guides/ingestion-tr.md companion. Per the project guideline, user-facing docs under docs/guides/ must ship as bilingual mirror documents.

Note: All files in docs/guides/ currently lack Turkish mirrors (not just this file), while docs/reference/ guides have implemented them. Ensure consistency across the documentation structure.

🧰 Tools
🪛 LanguageTool

[locale-violation] ~6-~6: In American English, ‘afterward’ is the preferred variant. ‘Afterwards’ is more commonly used in British English and other dialects.
Context: ...forgelm --data-audit afterwards to surface > length-distribution / lang...

(AFTERWARDS_US)

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

In `@docs/guides/ingestion.md` around lines 1 - 10, Add a Turkish mirror of the
user-facing guide by creating a new file named docs/guides/ingestion-tr.md that
is a bilingual mirror of docs/guides/ingestion.md: copy the content and
translate all user-facing text into Turkish, preserve any Markdown structure,
links (adjust localized link targets if needed), headings, code blocks and
metadata, and ensure the new file matches the same frontmatter/formatting
conventions as other -tr guides so it appears alongside ingestion.md in the docs
build.

Comment thread docs/guides/ingestion.md Outdated
Comment thread docs/roadmap.md
**Merged on `development`, tag + PyPI publish pending:** `v0.5.0` — Document Ingestion & Data Audit (Phase 11). `forgelm ingest` turns raw PDF/DOCX/EPUB/TXT into SFT-ready JSONL; `forgelm --data-audit` produces a CPU-only dataset quality report (length distribution, near-duplicate detection, cross-split leakage, PII flags) that feeds the EU AI Act Article 10 governance artifact automatically.

**Current milestone:** `v0.5.0` — Document Ingestion & Data Audit (Phase 11). PDF/DOCX/EPUB → JSONL, PII detection, near-duplicate audit.
**Previous:** `v0.4.0` — Post-Training Completion (2026-04-26). Inference primitives, interactive chat REPL, GGUF export, VRAM fit advisor, and deployment config generation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Date for v0.4.0 disagrees with CHANGELOG.

This line says v0.4.0 — Post-Training Completion (2026-04-26), but CHANGELOG.md line 105 records v0.4.0 as 2026-04-25. v0.4.5 is the 2026-04-26 release. Same fix needed in docs/roadmap-tr.md line 20.

📝 Proposed fix
-**Previous:** `v0.4.0` — Post-Training Completion (2026-04-26). Inference primitives, interactive chat REPL, GGUF export, VRAM fit advisor, and deployment config generation.
+**Previous:** `v0.4.0` — Post-Training Completion (2026-04-25). Inference primitives, interactive chat REPL, GGUF export, VRAM fit advisor, and deployment config generation.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Previous:** `v0.4.0` — Post-Training Completion (2026-04-26). Inference primitives, interactive chat REPL, GGUF export, VRAM fit advisor, and deployment config generation.
**Previous:** `v0.4.0` — Post-Training Completion (2026-04-25). Inference primitives, interactive chat REPL, GGUF export, VRAM fit advisor, and deployment config generation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/roadmap.md` at line 20, Update the date for the v0.4.0 roadmap entry
where the string "v0.4.0 — Post-Training Completion (2026-04-26)" appears so it
reads "v0.4.0 — Post-Training Completion (2026-04-25)" to match CHANGELOG.md;
make the identical replacement in the translated roadmap entry that contains the
same v0.4.0 string.

Comment thread docs/roadmap/phase-11-5-backlog.md
Comment thread docs/roadmap/phase-11-data-ingestion.md Outdated
Comment thread forgelm/ingestion.py Outdated
Comment thread tests/test_ingestion.py
Verified each of 30+ findings against current code. 17 real issues
fixed in this commit; 6 declined as cosmetic/refactor nits already
covered by docs/roadmap/phase-11-5-backlog.md; 1 finding (data_audit.md
fence missing language) was a false positive — fence already had
'json'. The reviewer's v0.4.0 date direction was inverted: tag + PyPI
upload both stamp 2026-04-26, so CHANGELOG entry [0.4.0] — 2026-04-25
was the outlier and gets aligned upward, not roadmap pulled down.

Code
- ingestion.py: mask_pii now optionally returns (masked_text,
  counts_dict) via return_counts=True; ingest_path uses that for
  pii_redaction_counts so a span matched by multiple patterns isn't
  double-counted (Bug 11). detect_pii() pre-pass removed.
- ingestion.py _extract_docx: walk doc.tables and flatten cell text
  in row-major order; docs already promised this behavior (Bug 30).
- data_audit.py credit_card: \d[ -]*? → \d[ -]* (greedy); same set
  match, no needless backtracking (Bug 19).
- data_audit.py _cross_split_overlap: f-string for the per-pair key
  + drop dead `if fp_a else 0.0` ternary that was guarded above.
- compliance.py: UnicodeDecodeError now caught when inlining
  data_audit_report.json so a corrupt UTF-8 audit file doesn't abort
  governance report generation (Bug 18).
- cli.py _run_ingest_cmd: ImportError now exits with
  EXIT_TRAINING_ERROR to match chat / export / etc. — missing extras
  are a runtime failure, not config validation (Bug 16).
- cli.py _run_data_audit: catch (FileNotFoundError, OSError) so
  PermissionError / ENOSPC / IsADirectoryError that bubble up before
  the per-split tolerance loop are reported, not opaque crashes (Bug 17).
- cli.py _run_ingest_cmd JSON output: structured pii_redaction_counts
  field surfaced (was only in notes string).

Docs
- docs/guides/data_audit.md: reconciled --output-format json behaviour
  description (top + later Note now agree: stdout = summary, disk =
  full report) (Bug 2). Encrypted-PDF wording in
  docs/guides/ingestion.md updated to match per-file skip-with-warning
  behavior the test asserts (Bug 4).
- docs/reference/usage.md: orphaned synthetic-config trailer moved
  back into the Synthetic Data Generation section (Bug 6).
- docs/reference/usage-tr.md: new "Doküman Yutma" + "Veri Seti
  Denetimi" sections mirror the EN additions (Bug 7).
- docs/guides/ingestion-tr.md, data_audit-tr.md: new TR mirrors of the
  Phase 11 user-facing guides (Bugs 3, 5).
- README.md: replace stale "430 passed (+34 skipped) across 30 test
  files" with version-stable wording; add TR mirror links to the new
  Phase 11 guides (Bug 13).
- docs/standards/architecture.md: drop hard-coded "Seventeen single-file
  modules" — count tracks the table (Bug 12).
- docs/roadmap/phase-11-data-ingestion.md: spec output schema
  {"messages":[…]} → {"text": …} matching shipped behavior; collapse
  duplicate blockquote; lift "### Tasks:" / "### Requirements:" /
  "### Delivery:" to ## level so heading levels don't jump (Bugs 10, 15).
- docs/roadmap/phase-11-5-backlog.md: "Recently landed" callout now
  enumerates the three pulled-forward items + states explicit count
  (12 deferred / 19 fixed / 3 landed = 34 original) for PR-summary
  consistency (Bug 9).
- CHANGELOG.md: v0.4.0 date 2026-04-25 → 2026-04-26 to align with the
  authoritative tag commit + PyPI upload timestamps (Bug 8).

Build
- pyproject.toml [ingestion] extra: add upper bounds (pypdf<6,
  python-docx<2, ebooklib<1, beautifulsoup4<5, langdetect<2) so a
  future major bump on any of those doesn't pull breaking changes
  into a `pip install --upgrade` flow.

Tests
- test_data_audit.py: type-ignore[arg-type] on detect_pii(None) /
  detect_pii(42) calls; remove duplicate _is_credit_card import
  (already imported at module scope); test_pseudo_split_fallback_warns
  now asserts the warning actually reached the logger (caplog),
  in addition to the existing report.notes check.

Declined (Phase 11.5 backlog scope, already documented):
- _resolve_input / _audit_split / ingest_path refactors for cognitive
  complexity (cosmetic; module-level concerns stay grouped per
  architecture.md's "one concern per file" guidance).
- O(n²) near-duplicate detection (already item 1 in backlog —
  LSH banding).
- All tests/test_*.py refactors not described above.

Verified locally: 718 passing tests (11 skipped pre-existing TRL+pypdf
extra paths), ruff clean, end-to-end CLI smoke confirms structured
pii_redaction_counts in --output-format json output.
All 12 findings verified by reproducing against current code (Findings
1, 2, 3, 4, 5, 8, 10 with concrete repro scripts; 6, 7, 9, 11, 12 by
direct code inspection). Fix shape chosen per finding rather than
mechanically applied — see per-bug rationale below.

Real bugs (high → medium severity)

- Finding 1: non-dict JSONL row crashed audit at row.get(). Fixed by
  treating array / scalar rows as `non_object_rows` in a SEPARATE
  counter (not conflated with null_or_empty per the reviewer's
  suggestion — operator visibility is sharper this way). Surfaces in
  splits[name]["non_object_rows"]; downstream length / fingerprint
  passes get an empty payload for those rows so stats stay honest.

- Finding 2: cross-split leak rate reported in the asymmetric
  direction (5/100=5% hides 5/10=50% test contamination). Fixed by
  reporting BOTH directions: leak_rate_<a> + leak_rate_<b> with
  matching leaked_rows_in_<split> counts. Notes line now picks the
  WORST direction explicitly ("Worst leak rate: 50.00% (train__test)").
  Single-pass implementation — no extra perf cost over the prior
  asymmetric scan.

- Finding 3: UnicodeDecodeError used to abort the whole audit.
  _read_jsonl_split now opens with errors="replace" + tracks U+FFFD
  occurrences as decode_errors so non-UTF-8 bytes don't kill the run
  but the operator still gets a structured signal. Mirrors the
  pattern ingestion._extract_text already uses (round 2's Finding 10).

- Finding 4: malformed JSONL lines were log-only — parseable subset
  was reported with no count of dropped lines. _read_jsonl_split now
  returns (rows, parse_errors, decode_errors) and audit_dataset
  surfaces both as splits[name]["parse_errors"] /
  ["decode_errors"] + an aggregate "Data integrity" note when any
  split has either. Article 10 audits now distinguish "this dataset
  has 1240 rows" from "this dataset had 1330 lines but 90 were
  silently dropped."

- Finding 5: sliding chunking emitted runt trailing fragments
  (text=205, chunk=200, overlap=100 → 3 chunks of [200, 105, 5]).
  Early-exit guard returns once the current window already covers
  end-of-text; example above now yields 2 chunks of [200, 105].
  The runt was a pure prefix of the prior chunk's overlap region —
  zero new context, but it inflated near-duplicate counts and
  skewed length stats.

- Finding 6: Article 10 governance auto-inlining looks for
  data_audit_report.json in training.output_dir, but the audit CLI
  defaults to ./audit/. When operators use defaults the inlining
  silently no-ops. Fixed by emitting a clear INFO log naming the
  expected path AND the exact command to populate it
  ("Run `forgelm --data-audit <dataset> --output <output_dir>`
  before training to populate it"). Smart-default coupling
  (auto-deriving --output from training.output_dir) was rejected as
  it'd violate the "audit module is independent of training config"
  separation; loud log is cheaper and just as effective.

- Finding 9: schema_drift_columns used rows[0] as the "base shape" —
  fragile when row 0 is the outlier (header row, single missing
  field, the non-dict case from Finding 1). Replaced with modal
  keyset via collections.Counter; the most common shape across rows
  becomes the base. False-drift on outlier-row-0 datasets eliminated.

- Finding 11: open(dst, "w", encoding=...) gave Windows CRLF line
  endings via universal newline translation. JSONL Files spec
  mandates LF, and CRLF breaks byte-exact reproducibility checksums
  between Linux/macOS and Windows runs of the same ingest. Both
  audit JSON write and ingest JSONL write now pass newline="\n".

- Finding 12: audit_dataset back-filled near_duplicate_pairs into
  the info dict that _audit_split returned — split between two
  functions, hidden coupling. Moved find_near_duplicates inside
  _audit_split so info is fully owned by one function. Keeps the
  contract obvious: _audit_split returns a complete info dict + the
  fingerprints + per-split PII counts.

Test coverage gaps

- Finding 7: DOCX and EPUB extractors had ZERO automated tests.
  Added TestDocxExtractor (paragraphs + row-major-flattened tables
  per Bug 30) and TestEpubExtractor (chapter content). Both fixtures
  built by the dep itself (python-docx, ebooklib) — no third-party
  fixture files. skipif(not _has(...)) so the test suite still runs
  cleanly without the [ingestion] extra installed.

- Finding 8: mask_pii(return_counts=True) had no direct tests.
  TestMaskPiiReturnCounts adds 5 tests: aggregation per pattern,
  first-match-wins (no double-count), invalid-Luhn-not-counted,
  non-string returns empty counts, back-compat one-arg form still
  returns a plain string. Locks down the compliance-evidence path.

- Finding 6 (cont): TestGovernanceAuditInlining adds 3 tests against
  forgelm.compliance.generate_data_governance_report — present /
  corrupt / missing audit JSON paths.

Doc fix

- Finding 10: docs/guides/data_audit.md (+ -tr) example imported
  asdict from forgelm.data_audit — works only because the module
  re-exports the symbol unintentionally. Fixed to
  `from dataclasses import asdict` so a future cleanup of the
  module's import surface doesn't rot the docs.

Verified: ruff clean, 737 passing tests (+19 vs. round 2), end-to-end
smoke confirms a single 5-line JSONL with array / scalar / parse-error
/ mojibake rows and a valid row produces a complete report (no crash)
with non_object_rows=2, parse_errors=1, decode_errors=1, sample_count=4.

@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: 2

♻️ Duplicate comments (1)
docs/guides/data_audit-tr.md (1)

61-70: ⚠️ Potential issue | 🟡 Minor

Same cross_split_overlap schema mismatch as the EN guide.

The TR mirror inherits the same incorrect example shape. Apply the matching fix here so the bilingual pair stays in sync (see the EN comment for the corrected JSON).

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

In `@docs/guides/data_audit-tr.md` around lines 61 - 70, The TR guide's JSON
example for "cross_split_overlap" uses the incorrect schema for "pairs" — update
the snippet so it matches the corrected schema used in the EN guide: keep
"hamming_threshold" and the same semantic values but replace the current "pairs"
shape with the corrected structure from the EN guide (apply the exact corrected
JSON for "pairs" as in the EN comment) so "cross_split_overlap",
"hamming_threshold" and "pairs" are consistent between languages.
🧹 Nitpick comments (5)
forgelm/cli.py (1)

1151-1151: Tiny cleanup: FileNotFoundError is a subclass of OSError.

SonarCloud's hint here is valid — listing both in the except tuple is redundant since FileNotFoundError is already an OSError. Drop the subclass for clarity:

-    except (FileNotFoundError, OSError) as exc:
+    except OSError as exc:

Behavior is unchanged; the comment about PermissionError / ENOSPC / IsADirectoryError already covers this correctly.

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

In `@forgelm/cli.py` at line 1151, The except clause in the error handling block
currently catches both FileNotFoundError and OSError redundantly; update the
handler in the relevant try/except (the line reading "except (FileNotFoundError,
OSError) as exc") to only catch OSError (i.e., remove FileNotFoundError from the
tuple) so the behavior remains the same but the code is clearer; leave the
existing comment about PermissionError/ENOSPC/IsADirectoryError unchanged.
forgelm/data_audit.py (1)

521-545: matched_b is built but never read — drop it (or use it to halve the second pass).

Inside _cross_split_overlap, matched_b accumulates every fp_b fingerprint that triggered a hit on the first sweep, but leaked_in_b is then computed by an independent any(hamming_distance(...) <= threshold for other in fp_a_set) over fp_b. The accumulator is dead code — the inline comment even acknowledges "matched_b as a starting point but a single pass over b is needed", but nothing actually consumes it.

Also note: fp_a_set = set(fp_a) doesn't accelerate the inner loop because hamming_distance still requires iterating elements (no O(1) lookup), so building the set is pure overhead.

♻️ Suggested simplification
-            fp_a_set = set(fp_a)
-            fp_b_set = set(fp_b)
-            # Single pass over fp_a captures both directions:
-            # rows in `a` whose nearest neighbour in `b` is within threshold,
-            # and the per-row matched fingerprints lift contributes to the
-            # `b`-side count.
-            leaked_in_a = 0
-            matched_b: set = set()
-            for fp in fp_a:
-                for other in fp_b_set:
-                    if hamming_distance(fp, other) <= threshold:
-                        leaked_in_a += 1
-                        matched_b.add(other)
-                        break
-            # Now count distinct b-rows whose nearest neighbour in `a` is
-            # close. We have `matched_b` as a starting point but a single
-            # pass over `b` is needed to find rows that match any `a`-row,
-            # not just the first `a`-row that triggered a match.
-            leaked_in_b = sum(1 for fp in fp_b if any(hamming_distance(fp, other) <= threshold for other in fp_a_set))
+            leaked_in_a = sum(
+                1 for fp in fp_a if any(hamming_distance(fp, other) <= threshold for other in fp_b)
+            )
+            leaked_in_b = sum(
+                1 for fp in fp_b if any(hamming_distance(fp, other) <= threshold for other in fp_a)
+            )

This drops the unused accumulator and the no-op set(...) calls without changing the reported metric.

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

In `@forgelm/data_audit.py` around lines 521 - 545, The matched_b accumulator and
the fp_a_set/fp_b_set set conversions in _cross_split_overlap are
dead/inefficient: remove matched_b and the set(...) allocations, compute
leaked_in_a by iterating fp_a and checking any(hamming_distance(fp, other) <=
threshold for other in fp_b) and compute leaked_in_b symmetrically by iterating
fp_b and checking against fp_a; keep using leaked_in_a, leaked_in_b, threshold,
hamming_distance and the existing report["pairs"][f"{a}__{b}"] population but
drop matched_b, fp_a_set and fp_b_set to simplify and avoid unnecessary work.
docs/guides/ingestion.md (1)

99-110: Tag the two unfenced code blocks for markdownlint MD040.

The directory-tree fence at line 99 and the CLI-usage fence at line 161 lack a language identifier — markdownlint flags both, and the rest of the file uses explicit fences (e.g. line 161 in data_audit-tr.md uses text).

📝 Proposed fixes
-```
+```text
 ./policies/
 ├── 2024_q1.pdf
-```
+```text
 forgelm ingest INPUT_PATH \
   --output FILE \

Also applies to: 159-171

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

In `@docs/guides/ingestion.md` around lines 99 - 110, Two code fences lack
language identifiers: the directory-tree block that begins with "./policies/"
and the CLI example that starts with "forgelm ingest ./policies/ --recursive
--output data/all_policies.jsonl"; add a language tag (use "text") to both
opening ``` fences so markdownlint MD040 is satisfied, and mirror the existing
style used elsewhere (e.g., the fenced example in data_audit-tr.md) to keep
consistency.
forgelm/ingestion.py (2)

308-434: Optional: extract the per-file processing block to tame cognitive complexity.

ingest_path packs file iteration, extractor selection, exception triage, chunking, PII masking, JSONL writing, and counters into one body — SonarCloud reports cognitive complexity 30. Pulling the inner loop into a small helper (e.g. _process_file(fpath, out_fh, …) returning chunks/chars/pii_counts) would let the entry point read top-down without changing behavior. Not blocking; flagging since the function is the primary touchpoint for future ingestion features (semantic chunking, additional formats).

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

In `@forgelm/ingestion.py` around lines 308 - 434, The ingest_path function is too
cognitively complex; extract the inner per-file work into a helper like
_process_file(fpath, out_fh, strategy, chunk_size, overlap, mask_pii) that
performs extractor selection, exception handling, skipping logic, chunk
generation, PII masking and JSONL writes, and returns counts (chunks_written,
chars_written, file_processed_flag, file_skipped_flag, per_file_format,
per_file_pii_counts); then simplify ingest_path to iterate files, call
_process_file for each, and aggregate returned counters (chunk_count,
total_chars, files_processed/skipped, format_counts, pii_redaction_counts,
notes) so the high-level flow reads top-to-bottom without changing behavior or
side effects.

371-378: Confirmed: ImportError re-raise ordering is correct.

Worth a one-line comment here since ImportError is a subclass of Exception and a future reader could be tempted to consolidate the two except clauses. Keeping except ImportError: raise ahead of the broad except Exception is what guarantees missing optional extras surface loudly instead of being silently downgraded to "skipped file". A short comment ("ImportError must propagate — missing extras are not a per-file skip") would prevent regressions.

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

In `@forgelm/ingestion.py` around lines 371 - 378, Add a one-line comment above
the ImportError except branch in the try/except around extractor(fpath)
explaining that ImportError must propagate so missing optional extras surface
(i.e., "ImportError must propagate — missing extras are not a per-file skip"),
and keep the existing ordering with except ImportError: raise before the broader
except Exception to prevent swallowing optional-dependency errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guides/data_audit.md`:
- Around line 61-70: Update the example JSON for the "cross_split_overlap"
payload so each pair contains the four directional fields the tests expect:
leaked_rows_in_train, leaked_rows_in_test, leak_rate_train, and leak_rate_test
(instead of the single "leak_rate"); specifically replace the current pair
object under "pairs" for "train__test" with entries for both directions and
their respective leak counts/rates, and apply the same change to the TR mirror
of the guide.

In `@forgelm/cli.py`:
- Line 238: The ingest subparser help string and the top-level epilog need to
include Markdown (.md) to match SUPPORTED_EXTENSIONS; update the help= value
that currently reads "Convert raw documents (PDF / DOCX / EPUB / TXT) into
SFT-ready JSONL." to include "Markdown (.md)" (or ".md") and likewise edit the
epilog text so Markdown is listed among supported input types; reference the
existing SUPPORTED_EXTENSIONS constant to ensure wording exactly matches the
supported types.

---

Duplicate comments:
In `@docs/guides/data_audit-tr.md`:
- Around line 61-70: The TR guide's JSON example for "cross_split_overlap" uses
the incorrect schema for "pairs" — update the snippet so it matches the
corrected schema used in the EN guide: keep "hamming_threshold" and the same
semantic values but replace the current "pairs" shape with the corrected
structure from the EN guide (apply the exact corrected JSON for "pairs" as in
the EN comment) so "cross_split_overlap", "hamming_threshold" and "pairs" are
consistent between languages.

---

Nitpick comments:
In `@docs/guides/ingestion.md`:
- Around line 99-110: Two code fences lack language identifiers: the
directory-tree block that begins with "./policies/" and the CLI example that
starts with "forgelm ingest ./policies/ --recursive --output
data/all_policies.jsonl"; add a language tag (use "text") to both opening ```
fences so markdownlint MD040 is satisfied, and mirror the existing style used
elsewhere (e.g., the fenced example in data_audit-tr.md) to keep consistency.

In `@forgelm/cli.py`:
- Line 1151: The except clause in the error handling block currently catches
both FileNotFoundError and OSError redundantly; update the handler in the
relevant try/except (the line reading "except (FileNotFoundError, OSError) as
exc") to only catch OSError (i.e., remove FileNotFoundError from the tuple) so
the behavior remains the same but the code is clearer; leave the existing
comment about PermissionError/ENOSPC/IsADirectoryError unchanged.

In `@forgelm/data_audit.py`:
- Around line 521-545: The matched_b accumulator and the fp_a_set/fp_b_set set
conversions in _cross_split_overlap are dead/inefficient: remove matched_b and
the set(...) allocations, compute leaked_in_a by iterating fp_a and checking
any(hamming_distance(fp, other) <= threshold for other in fp_b) and compute
leaked_in_b symmetrically by iterating fp_b and checking against fp_a; keep
using leaked_in_a, leaked_in_b, threshold, hamming_distance and the existing
report["pairs"][f"{a}__{b}"] population but drop matched_b, fp_a_set and
fp_b_set to simplify and avoid unnecessary work.

In `@forgelm/ingestion.py`:
- Around line 308-434: The ingest_path function is too cognitively complex;
extract the inner per-file work into a helper like _process_file(fpath, out_fh,
strategy, chunk_size, overlap, mask_pii) that performs extractor selection,
exception handling, skipping logic, chunk generation, PII masking and JSONL
writes, and returns counts (chunks_written, chars_written, file_processed_flag,
file_skipped_flag, per_file_format, per_file_pii_counts); then simplify
ingest_path to iterate files, call _process_file for each, and aggregate
returned counters (chunk_count, total_chars, files_processed/skipped,
format_counts, pii_redaction_counts, notes) so the high-level flow reads
top-to-bottom without changing behavior or side effects.
- Around line 371-378: Add a one-line comment above the ImportError except
branch in the try/except around extractor(fpath) explaining that ImportError
must propagate so missing optional extras surface (i.e., "ImportError must
propagate — missing extras are not a per-file skip"), and keep the existing
ordering with except ImportError: raise before the broader except Exception to
prevent swallowing optional-dependency errors.
🪄 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: 2a49dab8-70b0-4cf1-9a6c-70adaa461cdd

📥 Commits

Reviewing files that changed from the base of the PR and between 0889fd6 and 6121c9a.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • README.md
  • docs/guides/data_audit-tr.md
  • docs/guides/data_audit.md
  • docs/guides/ingestion-tr.md
  • docs/guides/ingestion.md
  • docs/reference/usage-tr.md
  • docs/reference/usage.md
  • docs/roadmap/phase-11-5-backlog.md
  • docs/roadmap/phase-11-data-ingestion.md
  • docs/standards/architecture.md
  • forgelm/cli.py
  • forgelm/compliance.py
  • forgelm/data_audit.py
  • forgelm/ingestion.py
  • pyproject.toml
  • tests/test_compliance.py
  • tests/test_data_audit.py
  • tests/test_ingestion.py
✅ Files skipped from review due to trivial changes (3)
  • docs/roadmap/phase-11-5-backlog.md
  • docs/reference/usage.md
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/standards/architecture.md
  • pyproject.toml
  • docs/roadmap/phase-11-data-ingestion.md

Comment thread docs/guides/data_audit.md
Comment thread forgelm/cli.py Outdated
…tors

Round-4 reviewer surfaced 8 inline findings and SonarCloud independently
flagged 5 cognitive-complexity violations + 4 type-mismatch warnings + 2
hardcoded-credential warnings + 1 weak-hash warning on the same surface.
Three independent signals on the same code = signal, not noise. The
ingest_path / _audit_split refactors I previously declined twice are
applied this round.

Inline-comment findings (small)

- Finding 1+2 (data_audit.md / -tr.md L61-70): cross_split_overlap JSON
  example used the OLD `leak_rate` schema — updated to the dual-direction
  shape the code now emits (leaked_rows_in_<split>, leak_rate_<split>).
  Both EN and TR mirrors fixed.
- Finding 3 (cli.py L238 + L302): `forgelm ingest --help` and the main
  parser epilog mentioned PDF/DOCX/EPUB/TXT but not Markdown, even though
  SUPPORTED_EXTENSIONS includes .md. Both strings now read
  "PDF / DOCX / EPUB / TXT / Markdown".
- Finding 4 (ingestion.md L99-110): unfenced directory-tree code block
  got `text` language tag for markdownlint MD040.
- Finding 5 (cli.py L1151): `except (FileNotFoundError, OSError)` →
  `except OSError` (FileNotFoundError is a subclass; the catch was
  redundant). Comment refreshed.
- Finding 6 (data_audit.py L497-545): _cross_split_overlap had a dead
  `matched_b` accumulator + redundant set conversions. Extracted
  `_count_leaked_rows` + `_pair_leak_payload` helpers; the orchestrator
  is now a clean nested loop. Behavioural identity verified by tests.
- Finding 7 (ingest_path refactor): previously declined twice. SonarCloud
  raised cog complexity 30 → 15 cap, third independent signal. Applied
  this round — _process_one_file helper extracts the per-file
  extract→chunk→mask→write block; ingest_path is now an aggregation
  loop over _FileOutcome dataclasses. Behaviour identical.
- Finding 8 (ingestion.py L373): inline comment now states why
  ImportError must propagate (missing extras → EXIT_TRAINING_ERROR via
  the CLI wrapper, not a per-file skip).

SonarCloud cognitive-complexity refactors

- _audit_split (cog 31 → ~10): extracted four helpers — _compute_schema
  (modal-keyset drift detection + non_object_rows count),
  _compute_payloads (text + null/empty), _compute_top_languages,
  _compute_fingerprints (with progress logging), _aggregate_pii.
- audit_dataset (cog 19 → ~10): extracted _process_split returning a
  _SplitOutcome dataclass + _pii_summary_notes + _cross_split_leak_notes.
- _resolve_input (cog 21 → ~5): extracted _scan_canonical_split_files
  + _scan_pseudo_split_files + _resolve_directory_splits orchestrator.
- _cross_split_overlap (cog 19 → ~6): see Finding 6 above.
- generate_data_governance_report (cog 20 → ~5): extracted
  _build_text_length_stats + _build_split_info + _governance_section
  + _maybe_inline_audit_report. Each helper owns one slice.

Each helper is private (underscore prefix) and individually unit-
testable; existing tests pass unchanged.

SonarCloud security/typing warnings

- Weak hash (data_audit.py L258): MD5 → BLAKE2b. Use is non-crypto
  (simhash bit-mixing) — `usedforsecurity=False` was set but Sonar
  doesn't read that flag. BLAKE2b is on the modern allowlist, faster
  than SHA-256, and natively supports digest_size truncation (no slice
  needed). Fingerprint collisions identical in distribution; existing
  hamming-distance tests still pass.
- Type mismatch (test_data_audit L77/78/121/435): detect_pii / mask_pii
  signatures changed from `str` → `Any`. The functions ALREADY guarded
  `if not text or not isinstance(text, str): return ...` defensively
  for arbitrary JSONL row payloads — the type signature now matches
  reality, so callers passing None / int / list don't trip mypy /
  Sonar typing checks. Test `# type: ignore[arg-type]` suppressions
  removed (no longer needed).
- Hardcoded credentials (test_ingestion.py L274): renamed PDF fixture
  passwords from "secret" / "owner" to "fx-user" / "fx-owner" + added
  `# noqa: S105` comments + explanatory note that these are PDF
  authoring inputs to PdfWriter.encrypt, not real credentials.

Verification
- ruff format/check: clean
- pytest tests/: 737 passed, 8 skipped (no regressions; refactors are
  behaviour-preserving)
- End-to-end smoke: multi-split JSONL with parse error + non-dict row
  + cross-split leak produces a complete report with the new schema
  on cross_split_overlap.pairs.<a>__<b>; sliding chunker confirmed to
  emit no runt trailing chunks; mask_pii(int) defensively passes
  through unchanged.
@cemililik

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

386-393: Document the additional overlap <= chunk_size // 2 bound.

The docstring says only overlap must be < chunk_size, but _chunk_sliding also rejects overlap > chunk_size // 2 at lines 207-211 to prevent quadratic chunk explosion. Library users hitting that limit via ingest_path() get a ValueError that isn't reflected in the docstring contract.

📝 Proposed docstring update
         chunk_size: Soft size cap per chunk (characters).
-        overlap: Overlap window for the sliding strategy. Must be < ``chunk_size``.
+        overlap: Overlap window for the sliding strategy. Must satisfy
+            ``0 <= overlap <= chunk_size // 2`` (the upper half-cap prevents
+            pathological chunk explosion; e.g. ``overlap=199`` with
+            ``chunk_size=200`` would emit roughly one chunk per character).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@forgelm/ingestion.py` around lines 386 - 393, Update the ingest_path()
docstring to include the additional constraint enforced by _chunk_sliding:
besides overlap < chunk_size, document that overlap must also be <= chunk_size
// 2 to avoid quadratic chunk explosion; mention that violating this will raise
a ValueError from _chunk_sliding so callers should validate overlap accordingly
and include the rationale (prevent quadratic growth) and the exact bound
(overlap <= chunk_size // 2) alongside the existing description of overlap and
chunk_size.
tests/test_ingestion.py (1)

58-65: Stale comment — early-exit suppresses the 4th chunk.

The comment claims 4 chunks ("abcd", "cdef", "efgh", "gh") but _chunk_sliding's early-exit (lines 224-225 of ingestion.py) returns after "efgh" since start (4) + chunk_size (4) >= len(text) (8). The test still passes because only the first three are asserted, but the comment will mislead the next maintainer into expecting a runt "gh" tail.

📝 Proposed comment fix
         text = "abcdefgh"
         chunks = list(_chunk_sliding(text, chunk_size=4, overlap=2))
-        # step = 2 → "abcd", "cdef", "efgh", "gh"
+        # step = 2 → "abcd", "cdef", "efgh".
+        # The early-exit in _chunk_sliding suppresses the runt "gh" tail
+        # that would otherwise be a pure prefix of the prior overlap window.
         assert chunks[0] == "abcd"
         assert chunks[1] == "cdef"
         assert chunks[2] == "efgh"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_ingestion.py` around lines 58 - 65, The test comment incorrectly
states that _chunk_sliding will produce four chunks ("abcd", "cdef", "efgh",
"gh") even though _chunk_sliding early-exits and only yields three; update the
comment in test_overlap_preserves_context to reflect the actual output from
_chunk_sliding (e.g., step = 2 → "abcd", "cdef", "efgh") or alternatively change
the test inputs (chunk_size/overlap or text length) if you intend to assert a
final short tail, and reference the _chunk_sliding function when making this
change so the comment and assertions match the actual behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guides/data_audit.md`:
- Around line 168-173: The CLI synopsis code fence in docs/guides/data_audit.md
is unlabeled (MD040); update the fenced block to use the text language tag so it
matches the Turkish mirror and satisfies markdownlint: replace the opening "```"
with "```text" for the forgelm --data-audit PATH block (the block currently
shown between lines 168–173) so the fence is explicitly labeled as text and
mirrors docs/guides/data_audit-tr.md.

In `@docs/guides/ingestion.md`:
- Around line 161-171: The CLI usage fenced block for the "forgelm ingest"
command is missing a Markdown language tag; update the code fence that contains
the forgelm ingest usage to use the text tag (i.e., ```text) so it matches the
labeled-fence convention used elsewhere (e.g., the data_audit-tr guide) and
resolves markdownlint MD040.

---

Nitpick comments:
In `@forgelm/ingestion.py`:
- Around line 386-393: Update the ingest_path() docstring to include the
additional constraint enforced by _chunk_sliding: besides overlap < chunk_size,
document that overlap must also be <= chunk_size // 2 to avoid quadratic chunk
explosion; mention that violating this will raise a ValueError from
_chunk_sliding so callers should validate overlap accordingly and include the
rationale (prevent quadratic growth) and the exact bound (overlap <= chunk_size
// 2) alongside the existing description of overlap and chunk_size.

In `@tests/test_ingestion.py`:
- Around line 58-65: The test comment incorrectly states that _chunk_sliding
will produce four chunks ("abcd", "cdef", "efgh", "gh") even though
_chunk_sliding early-exits and only yields three; update the comment in
test_overlap_preserves_context to reflect the actual output from _chunk_sliding
(e.g., step = 2 → "abcd", "cdef", "efgh") or alternatively change the test
inputs (chunk_size/overlap or text length) if you intend to assert a final short
tail, and reference the _chunk_sliding function when making this change so the
comment and assertions match the actual behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4cf26ce-aa65-463f-a853-33393d0dbb12

📥 Commits

Reviewing files that changed from the base of the PR and between 6121c9a and ebfcd3d.

📒 Files selected for processing (9)
  • docs/guides/data_audit-tr.md
  • docs/guides/data_audit.md
  • docs/guides/ingestion.md
  • forgelm/cli.py
  • forgelm/compliance.py
  • forgelm/data_audit.py
  • forgelm/ingestion.py
  • tests/test_data_audit.py
  • tests/test_ingestion.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • forgelm/data_audit.py

Comment thread docs/guides/data_audit.md Outdated
Comment on lines +168 to +173
```
forgelm --data-audit PATH \
[--output DIR] \
[--output-format {text,json}] \
[--quiet | --log-level {DEBUG,INFO,WARNING,ERROR}]
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unlabeled fence — diverges from the Turkish mirror.

The CLI synopsis block at lines 168-173 has no language tag, which markdownlint flags (MD040). The Turkish mirror at docs/guides/data_audit-tr.md lines 170-175 already uses ```text for the same block, so there's a small bilingual divergence here. As per coding guidelines, "User-facing docs must be bilingual (English and Turkish) as mirror documents".

📝 Proposed fix
-```
+```text
 forgelm --data-audit PATH \
   [--output DIR] \
   [--output-format {text,json}] \
   [--quiet | --log-level {DEBUG,INFO,WARNING,ERROR}]
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/guides/data_audit.md around lines 168 - 173, The CLI synopsis code
fence in docs/guides/data_audit.md is unlabeled (MD040); update the fenced block
to use the text language tag so it matches the Turkish mirror and satisfies
markdownlint: replace the opening "" with "text" for the forgelm
--data-audit PATH block (the block currently shown between lines 168–173) so the
fence is explicitly labeled as text and mirrors docs/guides/data_audit-tr.md.


</details>

<!-- fingerprinting:phantom:medusa:nectarine:608628d8-10bc-4ea6-9d79-3a4d3c8888d0 -->

<!-- d98c2f50 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread docs/guides/ingestion.md Outdated
… comment)

All 4 inline findings verified against current code; 1 bonus stale CLI
flag found in the same scan and fixed alongside.

- docs/guides/data_audit.md L168-173: CLI synopsis fence got the `text`
  language tag. Aligns with the labeled-fence convention used in the
  TR mirror and silences markdownlint MD040.
- docs/guides/ingestion.md L161-171: same fix on the `forgelm ingest`
  CLI synopsis fence. While reading the same block, noticed
  `--strategy {sliding,paragraph,semantic}` was stale — `semantic` was
  removed from the CLI choice list in round-1 (it raises
  NotImplementedError; hiding it from the surface was deliberate).
  Updated the synopsis to match: `{sliding,paragraph}`. TR mirror was
  already correct.
- forgelm/ingestion.py ingest_path() docstring: now documents the
  half-chunk overlap cap (overlap <= chunk_size // 2) alongside the
  existing overlap < chunk_size rule, plus the rationale (prevents
  quadratic chunk explosion from --overlap 199 + --chunk-size 200)
  and the exact exception path (ValueError from _chunk_sliding).
- tests/test_ingestion.py test_overlap_preserves_context: comment said
  step would yield 4 chunks ending in "gh"; the round-3 early-exit fix
  in _chunk_sliding makes that runt impossible. Comment updated and
  the assertion tightened from `chunks[0..2] == "abcd"/"cdef"/"efgh"`
  to `chunks == ["abcd", "cdef", "efgh"]` so a future regression that
  re-introduces the runt would be caught instead of hidden by an
  incomplete index-only assertion.

Verified: ruff clean, 737 passing tests / 8 skipped (no behavioural
change), all four narrow doc/comment findings discharged.
@sonarqubecloud

Copy link
Copy Markdown

@cemililik
cemililik merged commit cee5b1f into main Apr 27, 2026
10 checks passed
cemililik added a commit that referenced this pull request Apr 28, 2026
…d-on-main state

PR #13 (Phase 12 — v0.5.2) is merged on `main`. This sweep aligns the
narrative-doc state with that fact and consolidates the post-merge
review-cycle fixes under a single follow-up section.

roadmap.md / roadmap-tr.md
- Phase 11.5 + 12 status descriptions: "merged to development" →
  "merged to main (PR #11/#12 / #13)" with the actual merge date.
- "Latest release on PyPI" line now correctly says v0.4.5 (PyPI
  state) instead of v0.5.0.  Adds explicit clarification that v0.5.0
  / v0.5.1 / v0.5.2 are merged to main but not yet tagged + published.
- Phase-count matches v0.5.3 plan: "3 phases (12.5, 13, 14) planned".
- TR mirror status icons updated to "🟡 Birleşti" matching the EN
  "🟡 Merged" — were previously prematurely marked "✅ Tamam".
- Mermaid diagram in roadmap-tr.md gains the missing P125 node and
  bumps merged-but-not-published phases to a lighter green.
- New cross-link to issue #14 (webhook SSRF DNS-rebinding TOCTOU)
  surfaces in both the v0.5.2 row and the v0.5.3 "Next" line as the
  tracked v0.5.3 hardening follow-up.

roadmap/phase-14-pipeline-chains.md / roadmap/releases.md
- "starts after Phase 12 lands" → concrete release event (`v0.5.2`
  PyPI tag is published).
- v0.5.1 + v0.5.2 release-page status both moved off "Merged on
  development" to "Merged on main … remaining release-engineering
  steps" with the issue #14 hardening reference on v0.5.2.

README.md
- Always-on secrets-scan list updated: was 7 families with the wrong
  "private-key headers" wording; now lists all nine families per
  `forgelm.data_audit.SECRET_TYPES` (full BEGIN…END for OpenSSH /
  PGP).

CHANGELOG.md
- New "Fixed — post-PR-#13 review-cycle batches (rounds 8-12)" block
  in [Unreleased] consolidating the post-merge inline-comment
  batches: audit log hardening, deployer-instructions Markdown
  injection, quality-filter denominator, numpy fast-path bits guard,
  sliding-overlap clamp, batch-tokenizer narrow-except, webhook
  redirect/secret-fallback safety, trainer governance failure
  visibility, compliance-manifest exception narrowing, strict
  messages-format validation, wizard ASCII regex flag, GGUF
  converter case-insensitive validation, markdown-chunker complexity
  refactor, bidirectional MinHash extraction, streaming length
  digest, and the doc/test-count drift sweep.  Pointers to issue #14
  for the remaining DNS-rebinding follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Apr 28, 2026
…d-on-main state

PR #13 (Phase 12 — v0.5.2) is merged on `main`. This sweep aligns the
narrative-doc state with that fact and consolidates the post-merge
review-cycle fixes under a single follow-up section.

roadmap.md / roadmap-tr.md
- Phase 11.5 + 12 status descriptions: "merged to development" →
  "merged to main (PR #11/#12 / #13)" with the actual merge date.
- "Latest release on PyPI" line now correctly says v0.4.5 (PyPI
  state) instead of v0.5.0.  Adds explicit clarification that v0.5.0
  / v0.5.1 / v0.5.2 are merged to main but not yet tagged + published.
- Phase-count matches v0.5.3 plan: "3 phases (12.5, 13, 14) planned".
- TR mirror status icons updated to "🟡 Birleşti" matching the EN
  "🟡 Merged" — were previously prematurely marked "✅ Tamam".
- Mermaid diagram in roadmap-tr.md gains the missing P125 node and
  bumps merged-but-not-published phases to a lighter green.
- New cross-link to issue #14 (webhook SSRF DNS-rebinding TOCTOU)
  surfaces in both the v0.5.2 row and the v0.5.3 "Next" line as the
  tracked v0.5.3 hardening follow-up.

roadmap/phase-14-pipeline-chains.md / roadmap/releases.md
- "starts after Phase 12 lands" → concrete release event (`v0.5.2`
  PyPI tag is published).
- v0.5.1 + v0.5.2 release-page status both moved off "Merged on
  development" to "Merged on main … remaining release-engineering
  steps" with the issue #14 hardening reference on v0.5.2.

README.md
- Always-on secrets-scan list updated: was 7 families with the wrong
  "private-key headers" wording; now lists all nine families per
  `forgelm.data_audit.SECRET_TYPES` (full BEGIN…END for OpenSSH /
  PGP).

CHANGELOG.md
- New "Fixed — post-PR-#13 review-cycle batches (rounds 8-12)" block
  in [Unreleased] consolidating the post-merge inline-comment
  batches: audit log hardening, deployer-instructions Markdown
  injection, quality-filter denominator, numpy fast-path bits guard,
  sliding-overlap clamp, batch-tokenizer narrow-except, webhook
  redirect/secret-fallback safety, trainer governance failure
  visibility, compliance-manifest exception narrowing, strict
  messages-format validation, wizard ASCII regex flag, GGUF
  converter case-insensitive validation, markdown-chunker complexity
  refactor, bidirectional MinHash extraction, streaming length
  digest, and the doc/test-count drift sweep.  Pointers to issue #14
  for the remaining DNS-rebinding follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Apr 29, 2026
…prehensive v0.5.0

Originally planned as four sequential PyPI tags (v0.5.0 / v0.5.1 /
v0.5.2 / v0.5.3). The four phases form one coherent surface (ingest →
polish → mature → polish) that's hard to use in parts; consolidating
into a single comprehensive v0.5.0 release keeps the user-facing
release notes focused while git history retains the four phases as
separate commit batches.

pyproject.toml
- version: 0.5.2rc1 → 0.5.0

CHANGELOG.md
- Collapse [Unreleased] + [0.5.2] + [0.5.1] + [0.5.0] (summary stub)
  into a single [0.5.0] — 2026-04-29 entry.
- Theme paragraph + consolidation note explaining the merger.
- Internal phase boundaries preserved as section markers
  (### Phase 12.5 / Phase 12 / Phase 11.5 / Phase 11) so reviewers can
  map back to PR history (#11, #12, #13, current).
- [Unreleased] is now empty (next minor picks up Phase 14).

README.md
- "Document Ingestion & Data Audit (v0.5.0 — polished in v0.5.1,
  matured in v0.5.2)" → "(v0.5.0 — Phases 11 + 11.5 + 12 + 12.5
  consolidated)".
- Drop "v0.5.2 added X" / "v0.5.1 added Y" / "Phase 12.5 polish
  (v0.5.3)" history phrasing — features now read as part of v0.5.0.

docs/roadmap.md + roadmap-tr.md
- Status table: four separate "🟡 Merged" rows for Phases 11 / 11.5 /
  12 / 12.5 collapsed into one combined row pointing at
  releases.md#v050. Phase 14 row reslotted from v0.5.3 to v0.5.1.
- "Latest PyPI: v0.4.5" wording trimmed (the v0.5.0/.1/.2 sentence
  about three pending tags is gone).
- "Next: v0.5.3" → "Next: v0.5.1" (Phase 14).
- Phase count: 16 complete + 3 planned → 17 complete + 2 planned
  (12.5 moves into the consolidated release; 13 + 14 remain planned).
- Mermaid diagram: P11 / P115 / P12 / P125 all point at the same
  V2[v0.5.0 pending] node; styling bumped to merged-but-not-tagged
  light-green.

docs/roadmap/releases.md
- v0.5.0 / v0.5.1 / v0.5.2 sections collapsed into a single comprehensive
  v0.5.0 entry organised by phase (### Phase 11 / 11.5 / 12 / 12.5).
- v0.5.3 (Phase 14) → v0.5.1 (Phase 14).

docs/roadmap/phase-{11-5,12-5,12-data-curation,14-pipeline}.md
- Consolidation note at the top of each historical phase plan
  ("originally targeted vX.Y.Z, ships as part of consolidated v0.5.0;
  see releases.md#v050"). Body kept intact as the historical planning
  trail.
- phase-14: Target release v0.5.3 → v0.5.1; "starts after v0.5.2 PyPI
  tag" → "starts after v0.5.0 PyPI tag".
- phase-12-5-backlog rows: "ships with v0.5.3" → "ships with v0.5.0".

docs/guides/{ingestion,data_audit}{,-tr}.md +
docs/reference/usage{,-tr}.md
- Drop the "v0.5.2 added" / "v0.5.1 promoted" history phrasing inside
  feature descriptions. Features now read as v0.5.0 capabilities;
  phase labels (Phase 11 / 11.5 / 12 / 12.5) are kept as breadcrumbs
  for reviewers.
- Section headings like "Document Ingestion (v0.5.0+; token-aware in
  v0.5.1; markdown + secrets-mask in v0.5.2)" → "(v0.5.0)".
- Inline comments in CLI examples drop "v0.5.2:" / "v0.5.3 (Phase 12.5):"
  prefixes since the features all ship in the same release.

notebooks/data_curation.ipynb
- Install pin: 0.5.2 → 0.5.0 (cells 1 + 16).
- "Phase 12.5 audit add-ons (v0.5.3)" markdown → "Other v0.5.0 audit
  add-ons".
- Section 2 header drops the "Phase 12.5 shorthand (v0.5.3)" framing
  for --all-mask; the flag is just part of the v0.5.0 release.

forgelm/cli.py
- One internal comment tweaked from "v0.5.1 consumers" to
  "any pre-Phase-12 JSON consumer" (the legacy envelope key still has
  a real reason to exist; the framing just stops referencing a
  release that won't exist as a separate PyPI tag).
- Phase labels in user-visible help text (Phase 11.5 / 12 / 12.5) kept
  intact — they're descriptive history, not version pins.

Verification
- ruff clean (78 files)
- 840 tests pass (13 skipped)
- 7/7 bilingual H2 parity (configuration / usage / distributed /
  data_preparation / architecture / ingestion / data_audit)

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

Wave 5 / Faz 30 Task J: a CI guard that catches the bug class we've
chased four times this wave (GH-8 verify-log, GH-11 benchmark,
GH-16 --export-bundle, GH-18 --target kserve, GH-20 --pii-locale,
F-W4-01 verify-audit --output-dir/--json, F-W4-PS-02 approve --run-id).

Tool design (749 LOC, pure stdlib):
- Discovers the live parser surface by spawning `python3 -m forgelm.cli
  --help` + `forgelm <sub> --help` for every shipped subcommand —
  resilient to internal `_parser.py` reorganisation (Option A in the
  task spec).
- Walks docs/{guides,reference,usermanuals/{en,tr}/} + README.md for
  fenced bash/shell code blocks containing `forgelm <sub> ...`
  invocations.
- Reports three drift classes: (1) subcommand not in parser surface;
  (2) flag not in parser surface for that subcommand; (3) flag value
  not in parser's `choices` list.
- Skips `docs/analysis/` (gitignored research) + `docs/marketing/`
  (gitignored strategy) by default.
- False-positive avoidance: skips synopsis lines (containing `[` or
  `(...|...)` choice indicators), and skips code blocks in
  "Wrong:"/"Don't:"/"Anti-pattern:"/"Legacy:"/"Yanlış:"-tagged
  context. Forward-reference framing (`planned`/`roadmap`/`future`/
  `not in v0.5.5`/`(planned)`) within ±3 lines of the cited line is
  also tolerated as a legitimate ghost callout.
- Exit codes follow `tools/` contract: 0 advisory / 1 strict.

Tests (298 LOC, 15 cases — 3:23 wall-clock since each spawns the
real forgelm parser surface):
- Real shipping syntax (`verify-audit ./outputs/audit_log.jsonl
  --require-hmac`) → exit 0.
- Each historical drift class regression-pinned (verify-audit
  --output-dir/--json, benchmark --model X, deploy --target kserve,
  chat --top-p).
- Forward-reference framing + Wrong:-tagged blocks → tolerated.
- --quiet suppression of the OK summary.
- Parser-surface discovery sanity tests (synthetic empty repo,
  missing-subcommand short-circuit).

CI wiring:
- New advisory-mode step in `.github/workflows/ci.yml` runs the tool
  with `continue-on-error: true` after the bilingual-parity step.
  Matches the `check_anchor_resolution.py` Faz 26 precedent: gate
  lands first, baseline cleanup follows, then a later commit flips
  to `--strict` fail-the-build mode.

Baseline drift on the live tree: 40 findings across 20 docs. Top
hits cluster in `docs/usermanuals/{en,tr}/reference/cli.md` (legacy
flag tables drifted from the audit/verify-audit/chat/deploy/ingest
parsers as Wave 2-4 reshaped them) and a handful of phase-13/14
"planned" subcommands (`forgelm trend`, `forgelm compare-runs`)
documented as if shipped. Cleanup is the next Wave 5 commit.

Validation
- ruff format / check — clean.
- python3 -m pytest tests/test_check_cli_help_consistency.py --no-cov
  -q — 15/15 pass in 3:23.
- python3 -m pytest --no-cov -q — full suite green (1428 tests now;
  +15 from this commit).

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

Wave 5 / pre-merge review artifact, matching the Wave 3 + 4 cadence:
each wave ships with a self-contained 4-agent review prompt that the
maintainer can launch verbatim before merging the wave PR.

Prompt covers Wave 5's full diff (122 files, ~9272 insertions across
7 commits e18baa0 → 6448c2d):

- §A: 50 new doc-triplet files (Task A) — CLI surface accuracy,
  audit events, symbol citations, bilingual parity.
- §B: tools/check_cli_help_consistency.py — tool correctness,
  cognitive complexity, the 15 pinned tests, strict CI flip.
- §C: anchor checker --strict CI flip + 36→0 baseline cleanup.
- §D: site/* v0.5.5 finalisation (5 pages + i18n keys).
- §E: README + CONTRIBUTING + CLAUDE.md final pass.
- §F: 10 standards files final pass.
- §G: roadmap final state + new phase-12-6-closure-cycle.md.
- §H: Tier 1 ghost-feature drift residuals (GH-11/016/018/020).
- §I: pyproject.toml 0.5.1rc1 → 0.5.5 (brought forward from Faz 33
  because check_site_claims.py --strict enforces site/code version
  match).
- §J: 5 strict guards baseline + pytest 1428 + dry-run + closure
  plan honesty.

Severity calibration unchanged from Wave 3 + 4: CRITICAL = data
corruption / audit loss / public exit-code break / alignment-vs-
certified mislead; HIGH = runtime crash on happy path or missing
test for documented contract; MEDIUM/LOW/NIT calibrated per the
prompt's own severity table.

Deliverable shape per agent: single Markdown report at
`docs/analysis/code_reviews/wave5-<agent-name>.md` with verdict +
severity counts + per-finding entries (severity tag, ID, file:line,
reasoning, suggested fix, regression test/guard).

Launch: 4 parallel sub-agents with the same prompt; each picks up
its focus list (§B/C/H/I/J for code-correctness, §A.1-3+§D for
privacy-security, §A.4+§E/F/G+§J for standards, §B.1-2+§C.1+§J.2
for test-rigour).

.gitignore updated to allow-list the new prompt under
docs/analysis/code_reviews/ (matches the wave3-followup-review-prompt
+ wave4-review-prompt + wave4-followup-review-prompt allow-list
pattern).

Validation
- python3 tools/check_anchor_resolution.py --strict — 264/264 OK
  (the new prompt's internal links all resolve).
- python3 tools/check_bilingual_parity.py --strict — 39/39 OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 6, 2026
Each prior wave (1, 2a, 2b, 3, 4) has its own CHANGELOG section
under [Unreleased] documenting the Faz that wave closed. Wave 5
was missed during the bulk commits; this commit adds the section
above the existing Wave 4 entry, ordered newest-first per the
project's Keep-a-Changelog convention.

Section covers all 7 Wave 5 commits (e18baa0 through a93ec45):

- Faz 30 Task A: 50 new doc-triplet files (11 reference docs + 5
  guides + 9 usermanual pages × EN+TR) + parity registry +16
  pairs.
- Faz 30 Task J: tools/check_cli_help_consistency.py (749 LOC,
  15 pinned tests) + advisory CI step + strict flip in fbb082d.
- Faz 30 Task N: anchor resolver --strict CI flip + 36→0
  baseline cleanup.
- Faz 30 Task O: Tier 1 ghost-feature drift residuals
  (GH-11/016/018/020) cleared.
- Faz 30 Tasks B + C: _meta.yaml +8 page entries + JS bundle
  rebuild (56 → 64 pages).
- Faz 30 Tasks D + E + F + G: site v0.5.5 final state +
  README/CONTRIBUTING/CLAUDE.md + roadmap + 10 standards files
  final pass.
- Faz 30 Tasks H + I + K + L + M: dispositions documented (incl.
  honest closure of Task K — `tools/regenerate_config_doc.py`
  was never built; the three strict guards lock the contract
  instead).

Migration notes section calls out:
- pyproject.toml version 0.5.1rc1 → 0.5.5 (brought forward from
  Faz 33 because check_site_claims.py --strict enforces site/code
  version match).
- Two new strict CI gates (anchor resolution + cli-help
  consistency).
- 8 new usermanual pages + 16 new bilingual doc pairs (no
  user-facing behaviour change).

Faz 33 release commit will rename [Unreleased] → [0.5.5] — DATE
and possibly fold the Wave 4 + Wave 5 sections into a single
release-grouped entry; until then [Unreleased] holds the work
that lands in v0.5.5.

Validation: anchor strict 264/264 OK, parity strict 39/39 OK,
cli-help strict 449/449 OK (the new section's Markdown is
clean — no broken anchors, no flag drift in the migration-notes
examples).

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

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

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

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

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

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

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant