Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
04b06a1
ci(faz-32): add optional .pre-commit-config.yaml
cemililik Apr 30, 2026
597f759
docs(faz-32): document optional pre-commit hooks
cemililik Apr 30, 2026
1fd44c7
ci(faz-31): tag-driven release matrix (3 OS x 4 Python) with SBOM art…
cemililik Apr 30, 2026
3da7f3d
docs(faz-31): document Release prep workflow + cross-OS substantiation
cemililik Apr 30, 2026
6b4a488
refactor(faz-10): tighten 6 enum-shaped config fields to Literal
cemililik Apr 30, 2026
0b420ee
test(faz-10): cover Literal validation for the 7 newly-tightened fields
cemililik Apr 30, 2026
c0d9305
refactor(faz-12): consolidate minimal_config fixture into tests/_help…
cemililik Apr 30, 2026
3967f09
docs(faz-25): document EN+TR supported / DE+FR+ES+ZH deferred policy
cemililik Apr 30, 2026
0f1ea89
fix(faz-25): trim site picker to EN+TR, resolve YOUR_DOMAIN, sparse-e…
cemililik Apr 30, 2026
8e19ef5
feat(faz-25): site-as-tested-surface CI guard (tools/check_site_claim…
cemililik Apr 30, 2026
e57aaf8
fix(faz-25): restore 6-language site picker; site supports DE/FR/ES/ZH
cemililik Apr 30, 2026
4bbb672
feat(faz-13): formalize --data-audit deprecation discipline
cemililik Apr 30, 2026
bc9707a
refactor(faz-11): port _print() indirection from chat.py to wizard.py
cemililik Apr 30, 2026
a38debb
feat(faz-9): staging directory landing for human approval gate
cemililik Apr 30, 2026
649307b
feat(faz-9): forgelm approve / reject subcommands
cemililik Apr 30, 2026
2a45402
docs(faz-9): align marketing claim and audit catalog with staging story
cemililik Apr 30, 2026
894d2d8
test(faz-9): comprehensive human approval gate test coverage
cemililik Apr 30, 2026
a3d1559
fix(faz-9): align notify_awaiting_approval with audit_event_catalog
cemililik Apr 30, 2026
cba8ad4
refactor(faz-14): split data_audit.py into data_audit/ package + remo…
cemililik Apr 30, 2026
9084da5
refactor(faz-15): split cli.py into cli/ package
cemililik Apr 30, 2026
1445419
refactor(faz-27): narrow except Exception in safety/trainer/judge/com…
cemililik Apr 30, 2026
e2e99c4
refactor(faz-27, c-55): regex-based MoE expert name resolver
cemililik Apr 30, 2026
1203b37
docs(faz-27): error-handling.md best-effort carve-out
cemililik Apr 30, 2026
94868ec
chore(faz-29): nit sweep — audit catalog placeholders + trailing whit…
cemililik Apr 30, 2026
b351e9a
docs(closure-plan): mark Wave 1 phases (9, 10-15, 25, 27, 29, 31, 32)…
cemililik Apr 30, 2026
2643db0
fix(wave1-review): address findings from dual-agent code review
cemililik May 1, 2026
62a5f80
fix(wave1-review): address PR review, Sonar, and Codacy findings (bat…
cemililik May 1, 2026
536be49
fix(wave1-review): address inline/duplicate/nitpick findings (batch 3)
cemililik May 1, 2026
f844c49
fix(quickstart): stop forwarding --output-format json to training sub…
cemililik May 1, 2026
806f1fc
fix(review-batch4): 11 findings from inline/duplicate/nitpick pass
cemililik May 1, 2026
f0912d6
fix(review-batch5): 5 findings from inline/nitpick pass
cemililik May 1, 2026
bea7cb9
fix(sonar-codacy): address open SonarCloud/Codacy findings
cemililik May 1, 2026
bea9747
fix(batch6): 6 review findings — approve idempotency, staging uniquen…
cemililik May 1, 2026
bc1e783
fix(batch7): 5 review findings — staging rfind, webhook type guard, c…
cemililik May 1, 2026
53ca318
fix(batch8): 4 review findings — reject log wording, sentinel cleanup…
cemililik May 2, 2026
6ed33a3
fix(review): gate_path absolute path, JSON emit order, nosec suppression
cemililik May 2, 2026
b60fbcc
docs(closure-plan): v2.2 — ghost feature analysis absorption
cemililik May 2, 2026
be7d656
fix(review): wave1 round-2 closure — 7 findings, 3 new tests
cemililik May 2, 2026
78bab20
fix(review): wave1 round-3 closure — 6 findings + ghost analysis trac…
cemililik May 2, 2026
55ca1be
fix(review): wave1 round-4 closure — 7 findings (2 rejected)
cemililik May 2, 2026
864e2a8
fix(review): wave1 round-5 closure — 9 fixes (3 deferred with rationale)
cemililik May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ jobs:
print('synthetic.py import: OK')
"

- name: Site-as-tested-surface guard (strict)
# Diffs site/*.html claims against the Python sources of truth
# (compliance artefacts, quickstart templates, GPU profile count,
# pyproject version). Fails the build on any drift; see
# tools/check_site_claims.py for what's checked and why.
run: python3 tools/check_site_claims.py --strict

- name: Bilingual doc H2 parity check
run: |
python -c "
Expand Down
103 changes: 72 additions & 31 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,73 +1,114 @@
name: Publish to PyPI
name: Release - Build, cross-OS validate, publish to PyPI

# Tag-driven release pipeline. A pushed v* tag fans out into:
# build -> cross-os-tests (3 OS x 4 Python = 12) -> publish
# Any matrix combo failure breaks the needs: chain so PyPI publish does
# not run unless every supported (OS, Python) pair installs the packaged
# wheel cleanly and runs the test suite green.

on:
release:
types: [published]
push:
tags: ['v*']

# No workflow-level permissions — each job declares the minimal set it needs.
# No workflow-level permissions; each job declares the minimum it needs.

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # checkout needs read access to the release ref
contents: read # checkout reads the tagged ref
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install build tools
run: pip install build
- name: Install build tooling
run: python -m pip install --upgrade pip build

- name: Build sdist and wheel
run: python -m build
- name: Build wheel + sdist
run: python -m build # outputs dist/*.whl and dist/*.tar.gz

- name: Verify package
- name: Verify package metadata
run: |
pip install twine
python -m pip install twine
twine check dist/*

- uses: actions/upload-artifact@v5
with:
name: dist
path: dist/

# TestPyPI: triggered by pre-release tags (e.g., v0.3.0-rc1, v0.3.0-beta)
# or releases created from test/development branches
publish-testpypi:
cross-os-tests:
needs: build
runs-on: ubuntu-latest
if: github.event.release.prerelease == true
environment:
name: testpypi
url: https://test.pypi.org/p/forgelm
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.10', '3.11', '3.12', '3.13']
runs-on: ${{ matrix.os }}
permissions:
contents: read # download-artifact reads workflow run state
id-token: write # required for trusted publishing to TestPyPI
contents: read # checkout reads the tagged ref for tests/ + tools/
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}

- uses: actions/download-artifact@v5
with:
name: dist
path: dist/
path: dist

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Install packaged wheel (NOT editable)
shell: bash # POSIX shell on Windows runners too
run: |
# Install exactly what PyPI users will pip install. Transitive
# dependencies resolve from PyPI; the local dist/ only contains
# the forgelm wheel, not its dependency closure (so no
# --no-index --find-links here).
python -m pip install --upgrade pip
python -m pip install dist/*.whl

- name: Install Linux-only extras (qlora, ingestion, eval)
if: runner.os == 'Linux'
shell: bash
# qlora pulls bitsandbytes which only ships Linux wheels.
# ingestion + eval are cross-platform but bundled here so a single
# conditional install covers the breadth-test surface on Linux.
run: python -m pip install 'forgelm[qlora,ingestion,eval]'

- name: Install pytest
shell: bash
run: python -m pip install pytest pytest-cov

- name: Test (CPU-only, no-network)
shell: bash
# test_cost_estimation is excluded per Faz 31 closure plan: it
# depends on a snapshot pricing fixture that drifts on a different
# cadence than the release matrix.
run: pytest tests/ -q --ignore=tests/test_cost_estimation.py

- name: Generate SBOM (CycloneDX 1.5)
shell: bash
run: python tools/generate_sbom.py > sbom-${{ matrix.os }}-py${{ matrix.python }}.json

- uses: actions/upload-artifact@v5
with:
repository-url: https://test.pypi.org/legacy/
name: sbom-${{ matrix.os }}-py${{ matrix.python }}
path: sbom-${{ matrix.os }}-py${{ matrix.python }}.json

# PyPI: triggered by stable releases (non-prerelease) from main
publish-pypi:
needs: build
publish:
needs: cross-os-tests # blocked until every matrix combo is green
runs-on: ubuntu-latest
if: github.event.release.prerelease == false
environment:
name: pypi
url: https://pypi.org/p/forgelm
permissions:
contents: read # download-artifact reads workflow run state
id-token: write # required for trusted publishing to PyPI
contents: read # download-artifact reads workflow run state
id-token: write # OIDC trusted publishing to PyPI
steps:
- uses: actions/download-artifact@v5
with:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/site-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ jobs:
run: python3 tools/build_usermanuals.py

- name: Verify the generated tree
# Only the languages in SUPPORTED_LANGUAGES (build_usermanuals.py) are
# emitted today — see docs/standards/localization.md → Supported
# languages. Deferred bags (de/fr/es/zh) are intentionally absent.
run: |
test -f site/js/usermanuals/_index.js
test -f site/js/usermanuals/en.js
for lang in en tr de fr es zh; do
for lang in en tr; do
test -f "site/js/usermanuals/${lang}.js" || { echo "missing ${lang}.js"; exit 1; }
done

Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/usermanuals-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,37 @@ jobs:
raise SystemExit(1)
print("OK — every declared page has an English source.")
PY

- name: Orphan-markdown detection (strict)
# The reverse of the smoke test above: a markdown file under
# docs/usermanuals/<lang>/<section>/<page>.md that is NOT declared
# in _meta.yaml will silently never appear on the site. Strict
# mode fails the build so abandoned drafts are caught before
# they merge to main.
run: |
python3 - <<'PY'
import pathlib, sys, yaml
meta = yaml.safe_load(pathlib.Path("docs/usermanuals/_meta.yaml").read_text(encoding="utf-8"))
declared = set()
for sec in meta.get("sections", []):
for page in sec.get("pages", []):
declared.add(f"{sec['id']}/{page['id']}.md")
root = pathlib.Path("docs/usermanuals")
orphans = []
for md in root.rglob("*.md"):
rel = md.relative_to(root)
parts = rel.parts
if len(parts) < 3:
continue
# parts = (lang, section, page.md, ...). Only the <section>/<page>.md
# portion is matched against _meta.yaml.
key = "/".join(parts[1:])
if key not in declared:
orphans.append(str(rel))
if orphans:
print("ERROR: markdown files not declared in _meta.yaml (strict orphan check):")
for o in sorted(orphans):
print(f" - {o}")
sys.exit(1)
print("OK — no orphan markdown under docs/usermanuals/.")
PY
37 changes: 37 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Pre-commit hooks for ForgeLM (optional, developer-installed).
#
# CI is the enforcement boundary: ruff and gitleaks already run as separate
# steps in .github/workflows/ci.yml. These hooks exist purely as a local
# ergonomic optimization for contributors who want fast feedback before push.
#
# Install:
# pip install pre-commit && pre-commit install
#
# Run all hooks against the whole tree:
# pre-commit run --all-files
#
# Skip a hook (e.g. when committing a known-test fixture):
# SKIP=gitleaks git commit -m "..."
#
# Versions are pinned per closure-plan-202604300906.md Faz 32.

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
- id: ruff-format

- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
128 changes: 128 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,68 @@ All notable changes to ForgeLM are documented here.
> Per-PR CHANGELOG entries below collapse into the v0.5.5 release
> notes at tag time.

### Added — Wave 1 closure (Faz 9, 11, 12, 13, 25, 31, 32 — see PR description)

- **Article 14 staging directory + `forgelm approve` / `forgelm reject` (Faz 9)** —
When `evaluation.require_human_approval=true`, the trainer now saves the
final adapters to `final_model.staging/` instead of writing to
`final_model/` before review. Two new CLI subcommands manage the gate:
`forgelm approve <run_id> --output-dir <dir>` atomically renames
`final_model.staging/` → `final_model/` (with a `shutil.move` fallback on
cross-device output mounts) and emits a `human_approval.granted` audit
event plus a `notify_success` webhook; `forgelm reject <run_id>` records a
`human_approval.rejected` event and leaves the staging directory in place
for forensic review. Both commands resolve the approver identity via
`FORGELM_OPERATOR` → `getpass.getuser()` → `"anonymous"`, mirroring
`AuditLogger.operator`. The `human_approval.required` audit event payload
now also carries `staging_path` and `run_id` so downstream tooling can
cross-check the approval against the originating run.
- **`evaluation.staging_ttl_days` config field (Faz 9)** — documents the
retention horizon for `final_model.staging/` after a `forgelm reject`;
default 7 days. Auto-deletion enforcement is deferred to Phase 21
(GDPR right-to-erasure); v0.5.5 surfaces the policy in the compliance
manifest only.
- **`forgelm.wizard._print` indirection (Faz 11)** — 85 `print()` calls
replaced with a testable `_print()` helper (mirrors the chat.py pattern).
Coverage omit list emptied; wizard is now visible to coverage measurement.
Closes F-code-105, F-test-003, F-code-019.
- **`tests/_helpers/factories.py` (Faz 12)** — single canonical
`minimal_config(**overrides)` factory replaces 4 scattered local
`_minimal_config` definitions and 7 `from conftest import` indirections.
Closes F-test-004, F-test-005, F-code-015.
- **`forgelm --data-audit` deprecation (Faz 13)** — legacy flag emits
`DeprecationWarning` + a `cli.legacy_flag_invoked` audit event;
scheduled for removal in v0.7.0. Closes F-code-107, F-business-024.
- **6 enum-shaped config fields tightened to `Literal[...]` (Faz 10)** —
`LoraConfig.bias`, `DistributedConfig.fsdp_backward_prefetch` /
`fsdp_state_dict_type`, `SafetyConfig.scoring`,
`ComplianceMetadataConfig.risk_classification`, `TrainingConfig.galore_optim` /
`galore_proj_type`. Pydantic now validates whitelist at parse time;
bespoke runtime validators removed. Closes F-code-101, F-compliance-105.
- **`tools/check_site_claims.py` (Faz 25)** — site-as-tested-surface CI
guard; AST-parses `forgelm/compliance.py`, `forgelm/quickstart.py`,
`forgelm/trainer.py`, `pyproject.toml` and diffs against site HTML to
catch claim/code drift. Wired into `ci.yml` (`--strict` mode).
- **`docs/standards/localization.md` "Supported languages" section (Faz 25)**
— codifies that EN+TR are authored at site AND user-manual levels, while
DE/FR/ES/ZH are site-translated only and the user-manual side falls back
to English via the i18n chain. Closes F-loc-001, F-loc-003, Theme α.
- **`.github/workflows/publish.yml` cross-OS release matrix (Faz 31)** —
tag-driven `build → cross-os-tests → publish` chain over 3 OS × 4 Python
= 12 combinations; packaged-wheel install (not editable); SBOM artifact
upload per combo; OIDC trusted publishing. Closes F-test-007.
- **`tools/generate_sbom.py` (Faz 31)** — stdlib-only CycloneDX 1.5
emitter; called from each `cross-os-tests` matrix combo to produce a
per-OS-and-Python SBOM artifact.
- **`.pre-commit-config.yaml` (Faz 32; optional)** — opt-in local hooks
(`ruff`, `ruff-format`, `gitleaks`, trailing-whitespace,
end-of-file-fixer, check-yaml/-toml, check-merge-conflict). CI keeps
enforcing the same checks; pre-commit is ergonomic optimization, not a
duplicate enforcement boundary. Closes F-test-008.
- `tests/test_human_approval_gate.py` — 15 new tests covering the staging
→ approve / reject flow, stale-staging detection, atomic-rename race,
cross-device move, audit chain integrity. Total suite: 951+ tests.

### Added — Foundation bundle (PR #19, Faz 1-8)

- `forgelm verify-audit` subcommand + library function
Expand Down Expand Up @@ -88,6 +150,72 @@ All notable changes to ForgeLM are documented here.
[split-design-data_audit-cli-202604300906.md](docs/analysis/code_reviews/split-design-data_audit-cli-202604300906.md)
(Faz 14-15 forward-looking).

### Deprecated

- **`forgelm --data-audit PATH`** — the legacy flag now emits a
`DeprecationWarning` and an `cli.legacy_flag_invoked` audit-log event
on every invocation. Behaviour is unchanged; the flag is scheduled for
removal in **v0.7.0**. Migrate to the `forgelm audit PATH` subcommand
(same output, same exit codes). See
[docs/standards/release.md](docs/standards/release.md#deprecation-cadence) for
the removal timeline.

### Changed (Wave 1)

- `WebhookNotifier.notify_awaiting_approval(run_name, model_path)` is now
wired into the human-approval gate so an operator-facing webhook fires the
moment the model is staged. Receivers can opt out via
`webhook.notify_on_awaiting_approval=false`.
- **`forgelm/data_audit.py` -> `forgelm/data_audit/` package (Faz 14)** —
the 3098-line monolith was split into a 14-module package
(`_optional`, `_types`, `_pii_regex`, `_pii_ml`, `_secrets`, `_simhash`,
`_minhash`, `_quality`, `_streaming`, `_aggregator`, `_splits`,
`_summary`, `_croissant`, `_orchestrator`) per the
cohesion ceiling in `docs/standards/architecture.md`. The public
`forgelm.data_audit.X` import surface — including the
test-touched private helpers (`_HAS_PRESIDIO`, `_get_presidio_analyzer`,
`_token_digest`, `_strip_code_fences`, `_row_quality_flags`,
`_read_jsonl_split`, `_count_leaked_rows`, `_find_near_duplicates_brute`,
`_is_credit_card`, `_is_tr_id`, `_require_presidio`,
`_PRESIDIO_ENTITY_MAP`, etc.) — is preserved by `__init__.py` re-exports
so external callers (`forgelm.ingestion`, `forgelm.wizard`) and the
test suite keep working without code changes. Closes F-code-103 (Major).
See
[split-design-data_audit-cli-202604300906.md](docs/analysis/code_reviews/split-design-data_audit-cli-202604300906.md)
§1 for the design.
- **`forgelm/cli.py` → `forgelm/cli/` package (Faz 15)** — the ~2300-line
monolith was split into a 24-module package (subcommands/, `_dispatch`,
`_training`, `_dry_run`, `_result`, `_resume`, `_logging`, `_exit_codes`,
etc.). The `forgelm.cli:main` entry point and `python -m forgelm.cli` are
preserved; dispatcher uses late-binding facade re-resolution so test
monkeypatches (`forgelm.cli._run_chat_cmd` etc.) keep resolving correctly.
Closes F-code-104. See split-design §2.
- **16 broad `except Exception` sites narrowed (Faz 27)** — `_streaming.py`,
`trainer.py`, `safety.py`, `judge.py`, `compliance.py`, `ingestion.py`
narrow to specific exception classes; 7 sites retained with `# noqa: BLE001`
and rationale comments per `docs/standards/error-handling.md` carve-out.
C-55 resolved: MoE expert-name resolver migrated from hardcoded substring
match to regex-registry (`_EXPERT_NAME_PATTERNS`) covering Mixtral, Qwen 3
MoE, DeepSeek-V3, Phi-MoE. Closes F-code-106.
- **Audit event catalog and CLI sample drift fixed (Faz 29)** — placeholder
`<TBD>` entries in `audit_event_catalog.md` filled; trailing-whitespace
cleaned; CLI help sample in `docs/reference/usage.md` brought in sync with
current subcommand surface.

### Removed

- **`[ingestion-secrets]` extra (`detect-secrets>=1.5.0,<2.0.0`)** —
reserved during Phase 12 for a follow-up integration that never landed.
The `detect-secrets` scanner expects file paths while ForgeLM audits
row-level JSONL streams, so the wire-up was rejected as architecturally
incompatible. The prefix-anchored regex set in
`forgelm/data_audit/_secrets.py` (9-family coverage: AWS, GitHub,
Slack, OpenAI, Google API, JWT, OpenSSH, PGP, Azure storage) is the
sole detection backend and stays the sole detection backend. Removed
the dead extra from `pyproject.toml`, the install snippet from
`README.md`, and the "fallback regex set" framing from the secrets
module docstring. Closes C-53 (deferred from v0.5.0).

---

## [0.5.0] — 2026-04-30
Expand Down
Loading