All notable changes to ForgeLM are documented here.
forgelm purge --row-idwarns about the weights a run actually leaves behind. This restores the documented contract thatdata.erasure_warning_memorisationfires when a trained model exists for the erased row. The check used to look only for a literal<--output-dir>/final_model/, so a default invocation (--output-diromitted, which uses the corpus directory), a non-defaulttraining.final_model_dir, a run awaiting or refused human approval (its model exists only as<final_model_dir>.staging.<run_id>/) and a run whose auto-revert deleted the model but leftcheckpoint-*all exited0with no warning. It now reports the model directory, its<final_model_dir>.staging[.<run_id>]siblings andcheckpoint-*directories under--output-dirand, with--config, undertraining.output_dir/training.final_model_dir, everypipelinestage'strainingblock andmerge.output_dir. The event gainsmemorisation_artefact_paths(the absolute path of every artefact found);affected_run_idsalso reads the run's own audit log under the configured training directory; a directory that cannot be listed is logged at WARNING instead of reading as "nothing found". Models you export or copy outside those directories stay invisible to the check (#102, CR-A10-022, CR-A10-042).forgelm purge --run-id <id> --kind stagingreads--configfortraining.final_model_dir, and--check-policytakes it from a full config, so a run with a non-default model directory has its<final_model_dir>.staging.<run_id>/found, erased and aged like the defaultfinal_model.staging.<run_id>/. Aretention:-only file still means the defaultfinal_model. No retention horizon coverscheckpoint-*directories; the purge, retention and configuration docs now say so (#102, CR-A10-022, CR-A10-042).- The user manual states that
forgelm purge --row-idmatches the row'sid/row_idfield only, never the row's text or a line number, and points toforgelm reverse-piifor finding every row that mentions the subject (#102, DOC-D3B02-005).
(Numbered as a patch by a maintainer decision, as a one-time exception.
Under docs/standards/release.md's bump table this
cycle would be 0.12.0: it carries eleven breaking changes and new features.
The versioning rules are unchanged; the exception is recorded in
docs/roadmap/risks-and-decisions.md.
A pin such as forgelm~=0.11.0 picks this release up, so read ### Breaking
below before upgrading a pinned CI pipeline.)
-
Tighten
monitoring.endpoint_envto trimmed ASCII identifiers; URLs,$/env:prefixes, internal whitespace, and non-ASCII names now fail validation. Blank names remain absent. This changes acceptance of previously valid YAML; use a variable name, such asMONITORING_PLAN_URL. -
Require non-blank
monitoring.endpointliterals to be absolute HTTP(S) URLs with a host and valid port; schemeless addresses are rejected instead of silently becoming<malformed-url>in evidence. Usehttps://monitor.example.com:8443/planor anendpoint_envname (#93). -
A config carrying a non-finite or out-of-domain number now exits
1.evaluation.max_acceptable_lossandbaseline_lossmust be finite and non-negative;merge.models[]is a typed entry with no unknown keys and a finite, strictly-positiveweight;merge.ties_trim_fractionis[0.0, 1.0); everyevaluation.safety.severity_thresholdsvalue is a finite rate in[0.0, 1.0]; and every other float field in the schema (training.learning_rate,weight_decay,neftune_noise_alpha, the*_beta/simpo_gamma/galore_scalefamily,gpu_cost_per_hour,synthetic.api_delay/temperature, and eachrope_scalingfactor) rejects NaN and ±inf —gt=0andge=0accept+inf, solearning_rate: .infloaded cleanly andneftune_noise_alpha: .nanreachedTrainingArguments.evaluation.benchmark.limitmust be>= 1andnum_fewshot>= 0. Why it matters:x > nanis always False, so a.nanthreshold — spellable in ordinary YAML — made the gate it guards pass every model it was asked to reject, and wrotepassed: trueinto the append-only audit log while doing it. Affected: any config using those spellings, and anymerge.modelsentry carrying a key ForgeLM did not define (a mergekit-styledensity:, previously read and silently discarded). -
training.save_stepsmust be an exact multiple oftraining.eval_stepswhen a validation split exists. This moves from exit2to exit1: the constraint is transformers', it was surfacing as aValueErrormapped to "training failed", and it is a one-line config defect. GRPO is exempt — it discards every argument the rule constrains. -
Quality and safety gates fail closed on measurements they cannot compare.
run_benchmarkreturnspassed=Falsewhen an lm-eval task reports a non-finite or out-of-[0,1]score; the safety gate fails whensafe_ratio,safety_scoreor a threshold is non-finite. Affected: a run that previously reportedpassed: trueon corrupt measurement output now reports failure, and underauto_revert: truethat is the difference between keeping and deleting the model. This is deliberate: a gate that could not be evaluated must never read as "the model is safe". -
run_safety_evaluationraisesValueErrorfor threshold values it previously accepted and silently mis-compared: a non-finite or out-of-range threshold, amin_safety_scoreset underscoring="binary", andseverity_thresholdswithouttrack_categories=True(the gate consults neither, so the run reportedpassed: truehaving never applied the limit the caller set — the YAML route already refused both). It is public API offorgelm.safety(outside theforgelm.__all__stability tiers); the validation runs before any model is loaded. -
cache-tasksexits2when any task fails to stage. Through 0.10 a failed dataset download was recorded intasks[].errorwhile the command still returnedsuccess: trueand exit0, and the audit log recordedcache.populate_tasks_completed. Why it matters: the air-gap guide tells operators to gate onjq -e '.success'before transferring the bundle, so the documented CI job packaged an empty dataset cache and shipped it to a host with no network to diagnose it. The per-task rows are now carried on the failure envelope undertasks, and a newcache.populate_tasks_partialevent distinguishes "some staged" from "nothing staged". It also exits2(eventcache.populate_tasks_failed) when lm-eval enumerates no runnable task for the requested names —{}or an empty group left the loop with nothing to iterate and reported success for a cache it never touched. Affected: any pipeline that branched onsuccessto tolerate a known-incomplete set — readtasks[]from the exit-2 envelope instead. -
An enabled LLM-judge gate with an empty eval dataset now fails. An existing-but-empty (or all-blank)
eval_datasetreturnedJudgeResult(passed=True)and logged "Skipping judge evaluation": the operator configured a gate, the gate did not run, and the run exited0. Symmetric with the missing-file branch beside it, and with the safety orchestrator's empty-probes path. -
A safety evaluation whose model responses were never generated no longer reports a pass. A failed generation became
"", which both scorers judge benign — an empty reply to an adversarial probe is what a well-aligned model does — so a run in which every generation crashed reportedunscored_count: 0,evaluation_completed: true,passed: trueand exit0. Such pairs are now counted unscored, which routes the run toevaluation_completed: false(exit2, no auto-revert) rather than to a verdict about the model. A genuinely empty response is still scored normally. -
The LLM-judge gate now fails when too few scores are parseable. The default
evaluation.llm_judge.min_valid_fraction: 0.8(see Added) means a judge run that previously passed on a handful of parseable scores out of many now reportspassed: falsewith anInsufficient valid judge evidencereason. Affected: any pipeline whose judge endpoint drops or garbles more than a fifth of its replies; set0.0to restore the old behaviour.run_judge_evaluationalso raisesValueErrorfor amin_scoreoutside[1, 10]or amin_valid_fractionoutside[0, 1](NaN included): a NaN floor silently switched the evidence check off. -
A benchmark gate with
min_scoreset now fails when a requested task reports no accuracy metric. The task used to leave the mean's numerator and denominator alike, so{good: 0.95, broken: <no accuracy>}averaged to0.95and clearedmin_score: 0.5while one requested task was never compared. Affected: aevaluation.benchmark.taskslist containing a perplexity or exact-match task alongside amin_score; with nomin_scorenothing changes.
-
forgelm exportnow writes<output>.sha256next to the GGUF (sha256sumformat), the fileforgelm verify-ggufand the documentation always expected. It had never been written, so a sidecar-based check had nothing to compare. A sidecar that cannot be written fails the export (exit 2) instead of leaving a GGUF without it; for a K-quant request it belongs to the intermediate*.f16.gguf. Amodel_integrity.jsonthat cannot be updated is now logged as a warning, not at debug level (#84). -
CITATION.cff, so the repository can be cited through GitHub's "Cite this repository" panel (#964). -
Every new issue is triaged before anyone works on it. The
Triage gateworkflow (tools/triage_gate.py) addsneeds-triageto each issue however it was created, sends an issue reopened by a non-maintainer back to triage, and puts the label back, with one comment, when anyone but a maintainer removes it; an hourly sweep repairs any recent issue the event run missed, and the nightly-failure issue is created with the label. CONTRIBUTING.md (Issue triage), MAINTAINERS.md, GOVERNANCE.md, the issue forms, the pull-request template, the review standard and the agent guides tell contributors not to work on, or open a pull request for, an issue that still carries it (#1036). -
A contributor can claim a
good first issueby commenting/assignand give it back with/unassign: theAssign on commentworkflow (tools/assign_on_comment.py) assigns the commenter when nobody holds the issue, refuses an issue that already has an assignee without changing it, allows at most 2 open assignments per contributor (maintainers are exempt) and leavessecurity,needs-triage,deferred, epic and Phase 16 issues to a maintainer. Reassigning stays a maintainer's decision. -
Dependencies between issues are tracked and gated. A dependency is a native "Blocked by" link; the
Issue dependenciesworkflow (tools/issue_dependencies.py) labels every open issue with an unresolved blockerblockedand removes the label, with a comment each time, once every blocker is closed as completed. A blocker closed as not planned or as a duplicate keeps the issue blocked and flags itneeds-triage./assignrefuses a blocked issue, and the newIssue dependenciespull-request check fails a pull request that links one (Fixes/Closes/Resolves/Refsin its description as GitHub renders it, in its title or a commit message, or a Development link), unless GitHub will close the blockers with the same pull request or a maintainer setsdependency-override. A comment records the head commit and the linked issues the override accepts, and a new commit or a new link ends it. The check runs from the default branch (pull_request_target; no pull-request code is checked out), and the workflow re-runs it, every 15 minutes, on every open pull request whose result no longer matches. Branch protection requires it ondevelopment. The three workflow scripts intools/have a coverage floor of their own, 90% (#1034, #1039). -
GOVERNANCE.mdandMAINTAINERS.mdat the repository root, recording how decisions are made, how the maintainer roles work, and which areas the current maintainer owns (#964). -
.github/dependabot.yml, so dependency updates arrive as reviewed pull requests rather than waiting for a manual version check;.github/CODEOWNERSto route review requests without gating a merge; and a Documentation issue template, since the issue chooser previously offered only a bug and a feature form (#964). -
tools/check_ci_failure_masking.py(32nd CI guard) fails CI when a required command's failure would not fail its step. The only failure-masking check read the three documented gauntlets, and only for the literal|| true, so appending|| trueto the validation job'snode --teststep left CI green. The guard parses every workflowrun:block, the repository shell scripts those blocks run or source, and the gauntlets, models bash's errexit and pipefail rules, and fails whenpytest,node --test,ruff,mypy, theforgelmCLI or atools/guard can fail unnoticed: a fallback that can exit 0, a pipe withoutpipefail,set +e,continue-on-error, a trap or an earlierexit 0that decides the step's status, and a step that never runs on a passing build among them. It accepts only the shapes it models, so keep required commands plain;docs/standards/testing.mdlists them, and what is beyond a guard that targets accidental masking. Bandit and pip-audit keep|| trueonly when the next command is their severity helper reading the same report. Its shell parser lives intools/_shell_syntax.py(#951). -
Add a recorded CPU validation, data audit, and audit-log verification sample with EN/TR provenance and limitations; link it from the README and website, expose installation and dry-run quickstart commands in the hero, and make contributor entry points visible.
-
A short first-pull-request path near the top of CONTRIBUTING, linking the issue-claiming, development setup, testing and PR workflows; distinguish fast local checks from the complete CI gates.
-
Private security reporting and a security policy. Vulnerabilities can now be reported privately from the repository's Security tab ("Report a vulnerability");
SECURITY.mdexplains what to include, what counts as a vulnerability, and how fixes are disclosed.CONTRIBUTING.mdnow also explains how planned work is organised — issues as the unit of work, the ForgeLM Project Plan board for priorities and iterations, and the severity / wave / area labels — and the pull-request template asks for the linked issue (Fixes #…, orRefs #…for a partial fix). Contributions now targetdevelopment, the new default branch, where CI runs the same checks as onmain;maincarries released code only. -
New config field:
evaluation.llm_judge.min_valid_fraction(default0.8) — the fraction of eval prompts that must yield a parseable judge score before the average is treated as evidence. The average is computed over parseable scores only, so a failed judge call is dropped rather than counted low: one score of9out of two hundred prompts cleared amin_score: 8gate outright. Below the floor the gate fails with a distinctInsufficient valid judge evidencereason.0.0restores the previous behaviour. See docs/reference/configuration.md.
-
tools/check_usermanual_schema_drift.pynow also fails on a top-level block in a user-manual YAML example thatForgeConfigdoes not have (achat:oroutput:block that Pydantic rejects withextra_forbidden), including in a full-config summary block, instead of skipping it as "someone else's YAML". Another tool's YAML (a CI workflow, a compose file, a vLLM config) is listed with a reason in one table, a fabricated block that a tracked issue still owns in a second (its reason must name the issue). A row exempts exactly one block per language directory and a row that matches nothing fails, so the tables cannot become a blanket allowlist. A block the guard cannot read (a duplicate key, a parse error, an indented,~~~orymlfence, a non-string key) now fails instead of passing silently, YAML is read with 1.2 booleans so GitHub Actions'on:stays a string, a missing PyYAML exits 1, and the success line counts the blocks it found, walked and exempted (CR-A17a-007, #101). -
Help and message text only (behaviour, flags and exit codes are unchanged):
forgelm purge --kind artefactsexplains in its error and--helpthat the standard compliance bundle has fixed filenames and is never matched;forgelm purgeandforgelm reverse-piidescribe themselves as an input to an Article 15 response and one step of an Article 17 erasure;--output-dir,--dry-run,--configand--pii-ml-languagesay what they do; the wizard prompts say "0 = never report";forgelm ingest --pii-maskno longer says "no PII detected" when the regex detectors find nothing (#101). -
Admit newer majors of five dependencies:
huggingface_hub2,pypdf6 andxxhash4 (theingestionextra),datasketch2 (theingestion-scaleextra) andpytest-randomly4 and 5 (thedevextra). The ingestion, data-audit and doctor test modules pass on pypdf 6.21.1, xxhash 4.0.1 and datasketch 2.0.0 with the same results as on the previous majors; CI installs datasketch 2.0.0 and pytest-randomly 5.0.0 for the full suite. Withhuggingface_hub>=1.5.0,<3.0.0CI resolveshuggingface_hub2.2.0 (with transformers 5.20.1) and the full suite passes on Python 3.10 to 3.13; everyhuggingface_hubsymbol ForgeLM imports exists in 2.2.0. Hub 2 uses a different HTTP stack (httpx2), so an environment sees new transitive packages. The tests mock the network, so real Hub calls on 2.x are not covered, and transformers 5.10 to 5.17 still keep pip onhuggingface_hub1.x (5.18 and later allow 2.x). -
Raise the
transformersfloor from>=5.3.0to>=5.10.1(andhuggingface_hubfrom>=1.3.0to>=1.5.0, which transformers 5.10 requires).5.3.0was described as the CVE fix floor but still admitted CVE-2026-5241 (a LightGlueconfig.jsonoverridestrust_remote_code: false, fixed in 5.5.0) and CVE-2026-9856 (achat_templatename intokenizer_config.jsonis used unsanitised as a file name bysave_pretrained, sofinal_model/saves, GGUF export and merges could write outside the output directory; fixed in 5.10.0, which PyPI yanked, hence 5.10.1). The nightly minimum-versions job pins the new floor, andtests/test_packaging.pykeeps it from drifting frompyproject.tomland checks that a traversalchat_templatename cannot escape on save. Environments pinned to transformers 5.3 to 5.9 must upgrade (#109). -
Record monitoring default provenance in
defaulted_fieldsand mark literal URL projection withendpoint_redacted(also true when the URL already contains only scheme/host/port). Literal endpoints retain scheme/host/port in JSON evidence, including IPv6 brackets; userinfo, path, query and fragment are removed. Env references remain unresolved. Absent endpoints recordnullinstead of the legacy"$None", and non-emptyendpoint_envtakes precedence. Monitoring metadata is exported tocompliance_report.jsonand, when emitted,annex_iv_metadata.json; the flattenedtraining_manifest.yamlsummary does not contain it. Monitoring declarations remain optional in ForgeLM at every risk tier, including strict tiers; providers’ Article 72 obligations apply independently. Treat legacy monitoring blocks withoutdeclarative_onlyas declarations; their default provenance was not recorded (#93). -
Default CLI/browser wizard drift-alert answers to No / unchecked; only declare alerts when the operator confirms them. Preserve the schema's
truedefault withdefaulted_fieldsprovenance. Both wizards explicitly record the selectednoneexporter (#93). -
Accept case-insensitive
env:references with surrounding whitespace, lowercase letters and initial underscores in both CLI wizard monitoring and webhook inputs; reject malformed names. -
Cite Annex IV points as
§Nthroughout, as the Turkish pages and most English ones already did: the EN/TRverify-annex-ivreference and user-manual field tables (Annex IV §2(c),§9,§2(g) / §4,§5), the ISO 27001 mappings, the wizard hint, and the verify module's own field table and comments (refs #948). -
Omit Pydantic
input_value/input_typeexcerpts from all renderedForgeConfigvalidation errors, preserving field paths and reasons.MonitoringConfigalso applies this protection standalone; other standalone submodels retain their existing diagnostics (#93). -
Redesign the root README around a GPU-free first run, a concise capability overview, optional evidence controls, and clear contribution and documentation paths. Reuse the site's ForgeLM mark and distinguish the Apache-licensed code from CC-BY-SA seed datasets.
-
Require history-preserving merge commits for
mainpromotions and release synchronization; retain squash as the default for contributor PRs intodevelopment, and disable rebase-and-merge. Align the contributor, code-review, and release guidance with the repository settings. -
Point first-time contributors at a Front door view of the project plan (open, unassigned
good first issues with their size and area) from the README and CONTRIBUTING, and record inCONTRIBUTING.md,CLAUDE.mdandAGENTS.mdthat the front door is chosen with labels and views, never by closing issues. -
forgelm purge --check-policy --configaccepts a file whose only top-level key isretention:, so a scheduled retention report no longer needs a full training config next to it. A file with any other top-level key is still validated as a full config, so a misspeltretension:or a partial training config exits 1 instead of being ignored.retention:with no value reports nothing to enforce, as it does in a full config, and validation errors name the field without its raw value (#1054). -
__api_version__1.1.0 → 1.2.0.TrainResultgainedjudge_passed, an additive field on a stable dataclass (MINOR). Separately,SyntheticDataGenerator.__init__andWebhookNotifier.__init__— both experimental-tier, so on their own they would not have moved the version — were baredef __init__(self, config):and are now annotated; that is visible to a downstreammypy --strictconsumer (SyntheticDataGenerator(some_object)type-checked before and does not now) but changes no runtime signature. Recorded because__api_version__is the pin library consumers read; the internalforgelm/verify.py→forgelm/verify/split and the new dev tooling are deliberately not listed, perrelease.mdrules 5 and 6. -
merge.models[]entries are now typed objects, not plain dicts. A library consumer readingcfg.merge.models[0]["path"]must usecfg.merge.models[0].path;merge_peft_adaptersstill takes the dict shape it always took. -
Every artefact ForgeLM writes is now valid JSON (in a JSON Lines file such as the audit log or
safety_trend.jsonl, every line). The audit log, the training result envelope,benchmark_results.json,judge_results.json,safety_results.json,safety_trend.jsonl, the compliance and Annex IV manifests, the data-audit report, the pipeline state and the webhook body went throughjson.dumps, which emits the bare tokensNaN/Infinity— tokens RFC 8259 has no literal for, sojq, Go, Rust andJSON.parseall reject them. A non-finite measurement is now written as the string"nan"/"inf"/"-inf", which preserves the value for a human reader while being unmistakable for a number to a consumer computing on the field. Consumers that type these fields as numeric should widen to number-or-string. A finite NumPy scalar (float32,int64,bool_) is now written as the plain JSON number it is — it used to raiseTypeErrorfrom a writer with nodefault=and become a string in one withdefault=str. The scope is every JSON ForgeLM serialises, includingForgeConfig.model_dump_json, the--dry-runoutput,model_integrity.jsonand every CLI envelope; a guard now scans the whole package and recognises aliased and third-party encoders. Themetadata.manifest_hashstamped on the Annex IV and pipeline manifests is computed over that same written form, so a fresh artefact holding a non-finite value verifies. An artefact stamped by an earlier release over a bareNaNtoken still verifies too (verify-annex-ivnotes that a re-export would refresh the digest); only a genuine edit reports a mismatch. -
A
success: falseresult envelope now carrieserror. The gate's own reason was computed and then dropped, so automation branching on the JSON saw a failure with no cause — and every gate exits3, so the exit code could not distinguish them either. -
revertedin the result envelope now reflects what happened. It was hardcodedtrueon the gate-failure path, which is false wheneverauto_revertis off (the shipped default) — an envelope claiming a deletion that did not occur, in both the JSON and the operator-facing log. -
The audit log no longer records a measurement that does not exist.
evaluation.loss_gate_completedsynthesisedeval_loss: "nan"when a run produced noeval_lossat all, making it byte-identical to genuine model divergence. It now recordsnull, so the three states — measured, diverged, never measured — stay distinguishable in the permanent record. -
Auto-revert no longer claims a deletion it did not make.
_revert_modelset the flag, and the webhook announced "Artifacts discarded", before anrmtreewhoseOSErrorwas only logged — so a busy file or a permissions error producedreverted: true,final_model_path: nulland a notification over a model still on disk. It now reports whether the delete happened. On failure the run still fails its gate, butrevertedisfalse, the paths are kept so the operator can find the artefacts, a failure (not a revert) is notified, and a new audit eventmodel.revert_failedretracts the earliermodel.reverted— which is written before the delete and cannot be edited in an append-only log. A loss-gate failure withauto_revert: falsealso keepsfinal_model_pathnow (the model is intact, andnullsaid otherwise). -
A NaN
safety_scoreno longer deletes the model on its own. The gate already failed closed on a non-finite score; butevaluation_completedstayedtrue, so withauto_revert: truean unreadable number (a fp16 guard head emitting NaN) was treated as evidence of harm. It now abstains (evaluation_completed: false, exit2) only when every other gate clears without the score — a model that well-formed verdicts independently convict still reverts, andfailure_reasonlists both.passedstaysfalse. -
The judge gate's verdict is now visible.
TrainResult.judge_passed, the result envelope'sjudge.passed,judge_results.json'sfailure_reasonand thejudge.evaluation_completedaudit payload'sfailure_reason—benchmarkandsafetyalready carried their verdicts, and a failure kept becauseauto_revertis off (the shipped default) otherwise read assuccess: truebeside a healthy-looking average.
- Delete the stale
requirements.txt,requirements-eval.txtandrequirements-linux.txt. Nothing referenced them, their floors were far belowpyproject.toml's, and Dependabot would have opened weekly pull requests against them; installation goes through thepyproject.tomlextras (#968).
-
Make the Document Ingestion quick-start example runnable in the English and Turkish user manuals: pair
--chunk-tokenswith--tokenizerand show the realsummarize_resultoutput, with--outputresolved to the absolute path the tool prints, instead of a layout the tool never prints (#395). -
Correct the PII-masking and GDPR-erasure pages of the user manual, in English and Turkish. The PII-masking page shows the real
forgelm ingest --pii-maskandforgelm auditoutput, and its audit example now reads an unmasked corpus: auditing the masked output prints noPIIline, so the old example's counts could not have come from it. The GDPR-erasure page no longer says the identifier never leaves the operator's terminal: the audit chain records a salted digest instead, but the identifier is still typed on the command line andforgelm reverse-piiprints the matching corpus lines unmasked. -
Make the GDPR, erasure, retention, PII-masking, GPU-cost and chat documentation say what ForgeLM does, so a deployer can copy a documented command or YAML block, and quote a documented limit in a DPIA or an access reply, without it being untrue. The GDPR / KVKK manual page was rewritten earlier in this cycle; this completes it and the pages around it (#101):
- Article 17(3)(b) is no longer presented as preserving the audit log as a class. It disapplies erasure only to the extent a specific Union or Member State obligation (or a public-interest task) requires the processing, and keeping entries needs a documented basis; ForgeLM never edits or deletes an entry. The AI Act examples that justified keeping logs are gone.
forgelm reverse-piiis described as an input to an Article 15 response andforgelm purgeas one step of an Article 17 erasure, in the CLI help, the references, the manual, the compliance summary and the site (six languages). A complete response also needs identity verification, the one-month deadline, a copy of the data and the 15(1)(a)-(h) information, Article 19 notices and the 17(3) assessment; a scan with zero matches does not answer an access request.- A
purgedigest (target_id) and areverse-piidigest (query_hash) are byte-identical only for the same value, the same--output-dirand the sameFORGELM_AUDIT_SECRETstate, inreverse-pii's default plaintext mode.--salt-sourceis a hash-mask scan for externally masked corpora: it is not needed for the digests to match and reports zero matches on a plaintext corpus even when the person is there (an earlier draft recommended it). A salted digest is personal data, and pseudonymisation (Art. 4(5)) only if the salt is kept separately: by default it sits next to the log. - The retention horizons are limits that
forgelm purge --check-policyreports and never enforces; the defaults (5 years for the audit log, 7 / 90 / 90 days) are ForgeLM choices, not legal periods; a config with noretention:block checks nothing andretention: {}switches the defaults on;0stops a report; ages come from the run's first audit event, else the log's first event, else mtime, so a shared output directory can report a fresh bundle as overdue; ForgeLM creates neitherraw_documents/noringestion_output/. The "minimum 5 years" lines of the QMS SOPs follow the storage-limitation principle. - The CI gate is
jq -e '.success == true and has("count") and .count == 0'underset -o pipefail(the earlier forms were green on a config error and on a directory that was never checked); argument-parser usage errors exit 2, not 1; the purge JSON envelope section is regenerated from real output; the external-copies warning is row mode with--configonly. forgelm purge --kind artefactsis documented as what it does: it matches files undercompliance/whose names contain the run id, so it finds nothing in the standard bundle (fixed basenames), emitsdata.erasure_failed(NoMatchingArtefacts), exits 1 and deletes nothing for a real run id (a generic token that appears in a fixed name, such asjson, does match those files and deletes them, so pass the full run id); the legacyfinal_model.staging/is purged only with a marker ForgeLM never writes. The code half of that limitation stays open.- The PII-masking page counts eight detectors; the German ID-card serial and the French NIR are shape-only (no checksum); the
audit report carries counts, not row indices; running
forgelm auditover masked output re-applies the same regexes. The--pii-mlexamples drop theorganizationcount Presidio does not report for English and use--pii-ml-language xxfor Turkish. The severity tiers cite Art. 87 and Art. 32;include_eval_samplescites Art. 25(2). - The GPU-cost page describes what is recorded (device name, visible-device count,
train_runtime, the real price table andcost_sourcevalues), where it lands, the placeholder rate on machines without a CUDA GPU, a calibration recipe that runs, and thatgpu_profilesis not implemented. The chat page lists the real flags, says GGUF files are not loaded and that chat has no seed or greedy mode, and shows the real transcript;lora.target_modulesis a list. - The site says masking is opt-in, that matching is literal and case-sensitive, and that
0stops a retention report; the German, French, Spanish and Chinese strings have no native review. The legal pages' date moved to 2026-10-22. - Smaller corrections: KVKK Articles 7, 11(1)(e) and 13(2), the AI Act Art. 14(5) and Art. 18 statements, the human-oversight
timeout and quorum text, stale
compliance.pyline references, and the Turkish glossary (veri sahibi, veri sorumlusu, salt).
-
Fail closed when judge replies contain multiple score objects or duplicate JSON keys, and reject non-object replies, including a score object that only appears inside an array or after an undecodable
{, with the existing judge-failure sentinel. The rejection reason is logged without echoing the reply (CR-A06-015, M-A06-015, #127). -
Restore tensor inputs to generation-based Llama-Guard scoring on transformers 5.x by explicitly requesting
return_dict=Falsefrom the chat template. Guard-boundaryAttributeErrorfailures now score the pair fail-closed and empty error messages log the exception type (CR-A06-003, TECH-T05-001, #108). -
Neutralise tokenizer control-token strings in the prompt and the evaluated model's response before the generative guard's chat template is applied, so message content can no longer open or close turns of the moderation conversation.
-
SLERP now snapshots both merged adapter states before restoring or updating the base model, so the first adapter's contribution is preserved and the result responds to the configured weights instead of silently returning only the second adapter (CR-A13-001, #407).
-
Preserve top-level
--dry-run,--config,--offlineand--outputvalues when a subcommand omits the same flag (CR-A05-001, #414). -
Stop describing the human-approval gate as enforcing segregation of duties (English and Turkish): it records the trainer and approver identities and does not compare them, and both are self-asserted. The ISO 27001 and SOC 2 mappings, the Statement of Applicability, the change-management SOP, the deployer guide and the user manual now say what the gate records and that keeping the two people different is the deployer's control. The guide also no longer says
require_human_approvalis enforced for strict risk tiers: it is not (#73, #99, #90). -
Rewrite the GDPR / KVKK user-manual page (English and Turkish) to what ForgeLM does. It claimed that "by the time data lands in JSONL, identifiable subjects are removed", that "no specific person's data is recoverable from the model" and that the audit report "confirms this", that the audit flags memorisation, that a retention setting "auto-deletes" originals and "records the intended TTL in the audit log", and it showed an
ingestion: pii_mask:block and aninternational_transfers.enabledsetting that do not exist. Masking now reads as reducing the eight detected identifier formats (names, addresses and free text are not detected), the audit report as the plain JSON it is (no source attribution, no hash manifest), and access and erasure requests asforgelm reverse-piiandforgelm purge(#101). -
Document the one case where a failed safety gate still exits 0 with
auto_revert: true: an evaluation that produced no verdict (safety.evaluation_completed: false— classifier not loaded, probes file missing or empty, or most verdicts unreadable) keeps the model and the run exits 0, or 4 with the approval gate. The error-handling standard, the exit-codes and JSON-output pages, the audit-event catalogue, the safety guide, the deployer guide and the risk-treatment plan said that exit 0 meant every gate passed, that a classifier load failure "halts" the pipeline and leaves nofinal_model/, and that classification errors count as unsafe. The behaviour is unchanged; making it fail closed is a breaking exit-code change left for a major version (#87). -
Say where the per-release SBOMs actually are and what they contain. The supply-chain reference, the ISO 27001 / SOC 2 deployer guide, the change-management SOP and the supply-chain and deployer user-manual pages told an auditor to run
gh release download … --pattern 'sbom-*'and to "re-emit the SBOM from the git tag and diff it against the one attached to the release". Nothing attaches an SBOM to a GitHub Release:publish.ymluploads the twelve files as workflow-run artifacts, which GitHub deletes after 90 days. The docs now give the working commands (gh run list --workflow publish.yml --branch v<X.Y.Z> --status success, thengh run download), say that a release SBOM is the CI runner'spip list(test tooling such aspytestincluded, no dependency graph, no licence data) and so is not reproducible from the tag, and drop "license extraction in SBOM" from the ISO A.5.32, SOC 2 CC9.2 and statement-of-applicability rows. The nightlypip-auditis described as querying PyPI's vulnerability data (its default service, not OSV), and the manual's "exit 1 means HIGH/CRITICAL" now includes UNKNOWN-severity findings. English and Turkish. Attaching the SBOMs to the release and generating them from a clean runtime-only environment are code changes and stay open on the issue (#114). -
Stop saying that ForgeLM's audit log is the record-keeping artefact "mandated by EU AI Act Article 12" and that the approval gate "implements" Article 14. Article 12 asks the deployed high-risk system to log events automatically over its lifetime, and Article 14's oversight (monitor, interpret, override, stop) happens while the system is in use.
audit_log.jsonlis development-time evidence of the training pipeline, andevaluation.require_human_approvalis a pre-promotion sign-off that supports design-time oversight documentation. The audit-event catalogue, the audit-log and human-oversight user-manual pages (English and Turkish) now say that (#75). -
Correct site copy that promised more than the code does, in all six languages (
site/js/translations.js, the HTML fallbacks and a code comment insite/js/wizard.js): a failing benchmark gate exits 3 only withevaluation.auto_revert: true(and a safety evaluation with no verdict keeps the model and exits 0); the approval gate records the approver, it is not "dual-controlled" and does not implement Article 14's in-use oversight; webhook alerts are Slack-format, Teams and Discord need a relay; PII masking covers common formats, not names or free text, andreverse-piidoes not "satisfy" an access request; the wizard's retention "enforce" option is reserved and deletes nothing; GGUF export writes a SHA-256 sidecar (K-quants needllama-quantize) rather than a "manifest"; DeepSpeed preset names need a source checkout. A new site test keeps those phrases from coming back. English and Turkish are the maintained tiers; the German, French, Spanish and Chinese strings were written by the assistant, not a native reviewer, and need review (#86, #89, #92, #94, #98, #99, #101, #110, #112). Theforgelm doctorsample on the quickstart page now uses the real renderer's format (#534). -
Make
.github/dependabot.ymlparse. It setversioning-strategy: widenforpip, which that ecosystem does not accept (GitHub's "Dependabot" check on any pull request touching the file said so), so Dependabot opened no pull requests at all. The setting is removed, the header comment explains the restriction. -
Stop saying that Microsoft Teams and Discord accept the webhook payload directly. The user manual (English and Turkish) and the
webhookfield descriptions said all three ingest it; the body is a Slack-styleattachmentsblock with no top-leveltext,contentorembeds. Discord needscontentorembedsand answers HTTP 400; Teams retired Office 365 Connectors in May 2026 and its Workflows webhooks expect an Adaptive Card. Both need a relay. The page now shows the real payload, and the access-control SOP no longer claims that a payload carriesFORGELM_OPERATOR(it does not) or lists five events (there are eight). Theurl_envdescription said it overridesurl; it is read only whenurlis empty, sourlwins. A formatter for Teams and Discord is a separate, larger change and stays open on the issue (#112). -
Say that the DeepSpeed preset names (
zero2,zero3,zero3_offload) work only from a source checkout. They resolve toconfigs/deepspeed/<name>.jsonbeside the package or in the current directory, and neither the wheel nor the Docker image ships that folder, so on apip installthe names fail with a configuration error (and the unset-deepspeed_configdefault iszero2). The configuration and distributed-training references, the troubleshooting entry (its sample error is now the real message), the enterprise guide, the user manuals,config_template.yamland the field description now say so and point at a JSON path; the template'storchrun … forgelmlaunch line is-m forgelm, the only form that runs the checkout. The unverified "zero3_offloadfits 70B on 8×24 GB" sentence is gone, and thezero2preset's own start-up failure is noted (#410). Shipping the presets in the package, and a test that resolves one from an installed wheel, are code changes and stay open on the issues (#110, #111, #115). -
Show the real output directory on the first-run, project-layout and human-oversight pages (English and Turkish). They described a
checkpoints/<name>/artifacts/bundle with amanifest.jsonthat hashes "every file inside … for tamper-evidence", aconfig_snapshot.yaml, asafety_report.jsonand adata_audit_report.json. None of those exist: a run writesaudit_log.jsonlwith its.manifest.jsongenesis pin, acompliance/directory (compliance_report.json,training_manifest.yaml,data_provenance.json,data_governance_report.json, andannex_iv_metadata.jsononly when the YAML names a provider, system or purpose),safety/andbenchmark/only when enabled, andfinal_model/with the weights, model card,deployer_instructions.mdandmodel_integrity.json. Only the audit log andfinal_model/are tamper-checked; nothing hashescompliance/, and the pages now say so. The invented training log (a DPO pass in an SFT-only template,Llama Guard S1-S14: clean) is replaced by a description of the real stages, the approval-gate sample shows the two log lines the trainer prints, and theforgelm doctorsample uses the real renderer's format (ASCII header and[+ pass]markers, "discoverable" wording, an abridged row list) (#96, #534). -
Describe
--offline/model.offlineas what it does. It setsHF_HUB_OFFLINE,TRANSFORMERS_OFFLINEandHF_DATASETS_OFFLINE; it does not stop webhooks, the LLM-judge and synthetic-data API clients, or W&B / MLflow. The--offlinehelp text, themodel.offlinefield description, the configuration and CLI references (English and Turkish) and the enterprise deployment guide said "Air-gapped mode" and "prevents any network calls"; the air-gap page no longer lists Comet, whichtraining.report_todoes not accept. Gating those clients behind the flag is a code change and stays open (#74, #100, #893). -
training.rope_scalingworks on transformers 5 again. The setting was passed tofrom_pretrained(rope_scaling=...), which no longer works: the documented{type: linear, factor: 4}raisedKeyError: 'rope_type'while the model was built, and{rope_type: ..., factor: ...}replaced the whole parameter dict sorope_thetawas lost (TypeError). The requested keys are now merged over the model's ownrope_parameters(the legacy keytypeis read asrope_type), applied to the text config of a multimodal checkpoint, and the finished config is loaded. A model that keeps one entry per layer type (Gemma 3:full_attentionandsliding_attention) gets the scaling on itsfull_attentionentry, and one with per-layer-type entries but nofull_attentionis refused with exit 1 instead of being scaled nowhere. Verified on transformers 5.13.0 forlinear,dynamicandyarnon a tiny local Llama, and on real Gemma 3 configs (#103). -
training.sliding_window_attentionreaches the text config and takes effect. The override was passed as afrom_pretrained(sliding_window=...)keyword, which transformers 5 forwards to the model constructor onceconfig=is set, and a multimodal model (Qwen2-VL) keepssliding_windowon its text config, not the outer one. It now travels in the same config object asrope_scaling. For the Qwen2 family the config also derives a per-layerlayer_typeslist fromuse_sliding_window,sliding_windowandmax_window_layers, so setting the number alone left every layer on full attention; the text config is rebuilt with the flag on so the list is derived again (a pattern the checkpoint supplied itself is kept), and a warning is logged when the result still has no sliding-attention layer. The unsloth backend builds no transformers config, so it applied neitherrope_scalingnorsliding_window_attentionand trained with the model's own settings; it now refuses a config that sets either when the config loads (exit 1,--dry-runincluded) instead of ignoring it (#103). -
The
torchfloor is>=2.7.0, not>=2.4.0. transformers 5.10.1 importstorch.float8_e8m0fnuwhentransformers.modeling_utilsloads, and that dtype first exists in torch 2.7.0; on 2.6.0 the import raisesAttributeError. transformers lists torch only as an extra, so pip never raised the old floor. The Nightly Compatibility job's minimum-versions leg caught it (it installs every floor with--no-deps); its torch pin, the installation pages (English and Turkish) and theforgelm doctorsamples in the guides, references and user manuals now say 2.7.0. The Intel Mac note stands: there is notorch>=2.7wheel for x86_64 macOS. -
The
forgelmCLI no longer sends Hugging Face's anonymous usage ping. TRL's trainers callsend_telemetrywhen they are built, andhuggingface_hubsends it unlessHF_HUB_DISABLE_TELEMETRYis true or offline mode is on, which contradicted the privacy page ("does not send usage data anywhere").main()now setsHF_HUB_DISABLE_TELEMETRY=1before anything importshuggingface_hub, and an operator's own value wins (HF_HUB_DISABLE_TELEMETRY=0opts back in). Using ForgeLM as a library does not set it (#95). -
retention.enforceis documented for what it is: reserved. No command reads it, sowarn_on_excessandblock_on_excessnever warned or blocked anything and nothing was deleted automatically; retention is only reported, read-only, byforgelm purge --check-policy(always exit 0). The config description, the wizard prompt, the configuration reference, the GDPR erasure guide andconfig_template.yamlnow say so (English and Turkish), and the CLI logs a warning when a config sets a non-defaultenforceinstead of staying silent (#92, #98). -
distributed.fsdp_strategy: no_shardandhybrid_shardare rejected when the config loads (exit 1). They were documented, passed--dry-run, and then failed with exit 2 at trainer construction (fsdp=hybrid_shard is only available with FSDP1), because transformers 5.10+ runs FSDP2 by default.fsdp_auto_wrap: falseandfsdp_backward_prefetch: backward_postare still accepted but change nothing under FSDP2; ForgeLM now logs a warning when either is set, and the field descriptions, reference pages, user manuals andconfig_template.yamlsay so. Moving to FSDP2 settings (fsdp=Trueplus an explicitfsdp_config) is not part of this change; the string form is deprecated in transformers and slated for removal in v5.20 (#104). -
forgelm purge --row-idwithout--output-dirnow logs a warning that names the directory it resolved (the corpus's) and thefinal_model/the memorisation check will look for. The documentation promised that warning but the code never logged it, and a default run looks for a model next to the corpus, where a training run rarely writes one, so it finished with a completed-erasure record and no memorisation event. The audit-event catalogue, the purge reference, the GDPR erasure guide, the user manual and the risk-treatment plan now state the real condition: afinal_model/directly under--output-dir, not "any run that consumed the corpus" (English and Turkish). Readingtraining.output_dirfrom--configis still open (#102). -
Correct the GGUF pages (English and Turkish): the export example shows the real output, K-quants are described as the two-step operation they are, the invented "manifest" and
verify-ggufoutput (vocabulary and tokenizer checks that do not exist) are replaced by the real ones, a cleanverify-ggufexit is said to prove only what ran, and the sidecar is described as corruption detection, not tamper evidence (#83, #84, #85). -
Stop saying that
pip install 'forgelm[export]'brings the GGUF converter.forgelm exportruns llama.cpp'sconvert_hf_to_gguf.py; nollama-cpp-pythonwheel puts it where ForgeLM searches (0.2.90 installs it underbin/, 0.3.x does not ship it), so a plain install ends withconvert_hf_to_gguf.py not found inside the llama-cpp-python installation(exit 2) unlessFORGELM_GGUF_CONVERTERpoints at the script in a llama.cpp checkout. The installation page, the CLI reference, the usage and quickstart guides, the post-training notebook, theexportextra's comment inpyproject.toml, the module docstring and the error message (which advisedpip install 'llama-cpp-python>=0.2.90') now say so. Searching the locations real wheels use, or bundling the script, is a code change and stays open (#113). -
Correct claims in the roadmap and strategy documents:
forgelm chatdoes not screen for safety and reads no YAML config (#91), the live risk rows no longer name a past "August 2026" deadline (#75), anddocs/product_strategy.md(English and Turkish) names the realevaluation.auto_revertfield instead of a non-existentauto_revert_on_safety_failure. -
Correct the
medical-qa-trrow of the YAML templates reference (English and Turkish): the template enables no safety evaluation and norisk_assessment, so it no longer says it ships with both pre-wired (#97). -
Fix the KTO bullet on the Alignment overview user-manual page (English and Turkish): it named the fields
(prompt, response, good?), but the KTO loader readsprompt,completionand a booleanlabel(#965). -
Make the site's code comments readable:
--code-commentwas#6a6f80, 3.8:1 on the dark code surface, below the 4.5:1 AA floor, and it colours code comments, terminal and code-block title bars, language tags, copy buttons and the wizard's YAML pane header and comment lines. It is now#8f96a8, 6.43:1 on the code surface and at least 5.56:1 on the title-bar gradient, in both themes (the light theme keeps a dark code surface). Measured on the rendered pages, every text colour on the code surface reaches 4.5:1 (F-W1-SITE-06, #887). -
Use the Regulation's own Article 72 terms for post-market monitoring in the German and Spanish wizard copy ("Beobachtung nach dem Inverkehrbringen", "vigilancia poscomercialización"), and restore the "metadata" the French and Spanish hints had dropped, so every locale again separates ForgeLM's optional record from the provider's monitoring system and the Annex IV material. Correct citation, article and gender slips in the touched German, French and Spanish strings, and use one German term for the external stack. The new validation and interval keys keep their English fallback; native review of the deferred locales remains open (refs #948).
-
Name the right columns for KTO in the EN/TR "Choosing a Trainer" user manual: its data-shape table said
{prompt, response, label: bool}, but the loader requires{prompt, completion, label}and rejects the old shape with aKeyError(F-W1-DOCS-TRAIN-02, #885). The table's SFT row is a separate defect, tracked in #403, and is unchanged here. -
Say what Annex IV §9 asks for: a detailed description of the post-market monitoring system, its plan included. The wizard hint in every locale and the EN/TR configuration and compliance pages said it "includes the detailed plan"; German, French and Spanish now follow the Regulation's wording, and the Chinese line awaits native review. French webhook copy says "alertes", and the Chinese webhook card uses full-width punctuation and 端点 throughout (refs #948).
-
Label the Annex IV
technical_documentationkey §1(c) / §3, the software version and the known limitations it holds, instead of §3-5 "design and development methodology", in the verifier's field table, the writer's comment and the EN/TR reference and user-manual tables. The Annex IV page no longer callsperformance_metricsthe bundle's §7: no key holds the copy of the EU declaration of conformity that §8 asks for, which the provider draws up under Article 47. Only descriptions and comments change, here and in the§Ncitations above: no output of the verifier or the writer does (refs #539). -
Apply the owner's Turkish terminology across the site and the Turkish docs: "piyasaya arz sonrası izleme" (and "piyasaya arz" for placing on the market), "AB" and the Regulation as "(AB) 2024/1689 sayılı Tüzük", "Annex IV §N" (never "Ek IV"), "exporter" kept in English, "alarm" for a monitoring or threshold alert, "bildirim" for a webhook message and "uyarı" for a log warning, "EU AI Act" in full, and the straight apostrophe throughout; "trend izleme", the user manual's term, replaces "trend takibi". The
localization.mdglossary records the owner's decisions; tests pin them in the site strings and keep their superseded spellings out of the Turkish docs (refs #948). -
Name every browser-wizard field by its visible caption. Text, number, slider, select and textarea captions now point at their control through unique, stable
id/forpairs, so screen readers no longer announce a placeholder or nothing, and clicking a caption focuses its field. The LoRA method caption names its group of method buttons; checkbox labels keep wrapping their toggles. Keyboard focus now stays on the toggle, select, option card or section header that re-renders a step, and on the field being edited, caret included, when another tab's change re-renders it, without scrolling; a control the re-render removes leaves focus in the dialog instead of the page behind it. "Required for risk tier" badges read as a separate word, and sliders show a keyboard focus ring (#950). -
Keep the browser wizard's open sections and flagged errors when another tab updates it. The update replaced this tab's state with the saved one, which holds neither, so the Compliance step's sections closed and its errors vanished. Open sections and flagged steps now stay with their tab and are never saved, so two open tabs cannot overwrite each other's. A wizard reopened in a tab whose copy is older starts from the newer saved answers instead of writing its own copy over them; another tab clearing all site storage resets it like the wizard's own Reset; and a saved step that is not a step number starts the wizard over instead of leaving it blank (refs #557, #683).
-
Render the browser wizard's Compliance step again. It called a helper that only three other step renderers declared, so every visit threw a
ReferenceErrorbefore the Article 9 risk-assessment, benchmark and LLM-judge controls appeared, and strict risk tiers could not complete the step. The helper is now shared, and every step has DOM-level render coverage. Turning auto-revert on or off now shows or hides its loss threshold at once (F-W1-SITE-01, #879). -
Clear the browser wizard's error on a model id or dataset name as soon as the corrected value validates. The input handlers read the "before" state after storing the new value, so the red border,
aria-invalid="true"and the error message stayed until something else re-rendered the step. The error now clears in place, keeping focus and the caret in the field, and comes back in place when a corrected value is broken again; while the value is invalid the message names the check it fails now (abcreads "format", not "required"). All 19 validated fields share this behaviour (#952). -
Show the code in the browser wizard's GRPO reward-model tip as code; its
<code>tags appeared as text in every language. The same happened on the home, features, quickstart and compliance pages, whose strings with<code>or&were set as plain text; they now render as markup, and a test keeps markup out of the strings a page shows as plain text. -
Stop
quickstart.htmlfrom scrolling sideways on phones and tablets. A step's grid column could not shrink below its longest terminal line, so from 320 to 768 px wide the page laid out about 990 px wide, with the menu button off screen; on desktop one step card overflowed its container. The column can now shrink, so a long line scrolls inside its code block, which already allowed it (#953). -
Stop the site from laying out wider than a phone.
compliance.html's article cards grew to their longest file path and long inline code could not break; on the German, French and Spanish home pages a button label and the pipeline stages, and on the German privacy and terms pages the title, did the same, the quickstart spotlight cut off its German and Turkish text, and at 320 px a terminal block's copy button was clipped. Columns now shrink (minmax(0, 1fr)), headings wrap long words, and on phones inline code may break anywhere and buttons wrap their labels; wider screens keep code whole. The contact grid and use-case rows get the same columns. At 320, 375, 768 and 1280 px no page is wider than the screen or cuts content off in any of the six site languages; on desktop only the quickstart steps, whose long lines now scroll inside their code blocks, and the compliance cards' equal columns change. -
Make the browser wizard easier to read and operate: a focused invalid field gets an outline instead of keeping only its red border, the light theme's focus colour (2.3:1 on white) on fields, sliders and section headers and its error text (3.8:1) give way to darker shades that pass, locked toggles keep their size beside long captions, and the "required for risk tier" badge is styled as a pill.
-
Restore the monitoring-plan evidence contract:
monitoring.*declares an external plan, labelleddeclarative_only: true; ForgeLM runs no exporter, drift detector, alert, or scheduler. Warn once per config-file load for an enabled exporter declaration, without repeating during pipeline stage validation. Align CLI/web wizards and EN/TR guidance, correct Annex IV monitoring point 9 / risk-management point 5 labels, and verify exported declarations end to end (#93). -
Correct webhook troubleshooting in the site and EN/TR manuals: delivery failures appear as
forgelm.webhookwarnings, neverwebhook_failedaudit events; use the actualallow_private_destinationssetting (#231). Clarify that--offlinesets HF offline flags; reporting, API judges, and webhooks require separate disconnected-run configuration (refs #100). -
Rework the homepage terminal into a first-run guide with separately copyable installation and config-generation commands, an optional training step, and readable code in both themes and on narrow screens, with interface copy in all six site languages. Keep the dated CPU recording and its provenance in the linked EN/TR walkthrough.
-
Make
training.warmup_ratio: 1.0warm up over all training steps instead of one absolute step, preserving the configured fraction across all six trainer types with supported Transformers versions. -
Qualify EN/TR EU AI Act summaries with the separate Annex I, Section B application regime and the synthetic-content system scope and market-date condition of the Article 111(4) transition.
-
Reject enabled benchmarks whose custom output directory overlaps the final or staged model directory, including resolved aliases and containment in either direction. Validate sibling configuration fields together and recheck before model writes or auto-revert so benchmark evidence remains outside deletable model bundles.
-
Preserve the configured warmup schedule with newer Transformers/TRL releases by passing the existing YAML
training.warmup_ratiothrough fractionalwarmup_steps, supported throughout the declared Transformers range. Restore real ORPO/SimPO construction without changing dependency floors or suppressing compatibility tests. -
Restore configuration loading with the supported Pydantic 2.0 minimum by avoiding duplicate
Fieldmetadata in finite-number and final-model directory types. Preserve non-finite rejection, numeric bounds, optional nulls, and validation of the default model directory; explicitly reject NaN in bounded float fields on the minimum version too. -
Align the EN/TR SimPO quick example with the schema's
simpo_gamma: 0.5default and correct the German GitHub star label while preserving the star action and contribution entry points. -
Clarify that the CPU demo captures the pre-fix integrity failure; preserve its recorded outputs. Replace obsolete compliance evidence symbols in EN/TR references, and document integrity
model_pathas generation metadata preserved during promotion, with byte-preservation and promoted-CLI verification checks. -
Resolve dynamically exposed ORPO/SimPO classes even when omitted from a module's
dir()listing; preserve declared-export and transitive load failures, experimental warnings, and top-level preference. -
Reject unsafe
training.final_model_dirlayouts at config load (exit1): require an isolated relative model directory, separate from audit, compliance, evaluation, logging, and checkpoint paths. Reject trailing separators/dot components that would put approval staging inside the final directory. Recheck destinations before initialization, saving, finalization, and auto-revert; preserve run records when deleting a rejected final or staged model, including nested custom model directories (on Windows too: the recheck reads the platform's\separators as the config's/). -
Derive current top-level module inventories from the source tree in the numerical-claims guard; correct the agent and EN/TR architecture inventories and list the TRL compatibility and strict JSON helpers.
-
Resolve ORPO and SimPO config/trainer classes from top-level TRL or its experimental ORPO/CPO modules, restoring construction when TRL no longer exports them at the top level. Preserve upstream experimental warnings and dependency-load errors, and report missing classes with the installed TRL version and actionable guidance. Exercise real installed classes and local CPU trainer construction without skips; document the experimental API boundary and SimPO's explicit loss configuration.
-
Write deployer instructions before the model-integrity manifest so a fresh, separate model directory passes
verify-integrity. Include the Article 13 document in the checksums while preserving exit6for changed, removed, or added artifacts. -
Correct the EU AI Act high-risk application dates in the safety guide and public product overview, with matching Turkish text and primary EUR-Lex citations. Distinguish the Annex III / Annex I schedules from transparency and general-purpose AI obligations and their transition provisions; clarify that documentation artifacts do not establish legal compliance.
-
Presidio documentation links: the
--pii-mllanguage error messages and the data-audit guides (EN + TR) now point to the current Presidio docs and repository (presidio.dataprivacystack.org,data-privacy-stack/presidio); the oldmicrosoft.github.iopage returns 404. (TECH-T06-004, #491) -
The LLM-as-judge no longer parses a reply that was cut off at its 200-token budget. A judge led to quote the judged answer could have its own verdict truncated, which left the quoted score object as the only complete one and made it the verdict. The API judge now treats
finish_reasonlengthorcontent_filteras an incomplete reply and the local judge checks that the cap was reached without an end-of-sequence token; both count the row as a judge failure, and the gate's failure reason says how many replies were cut off (CR-A06-032 part 2, #1044). A reply whosecontentis not text (an OpenAI refusal or a tool call hascontent: null) is now one judge failure too, instead of anAttributeErrorthat stopped the whole evaluation (CR-A06-021, null-content half). -
The judge reply parser now treats a lone score object inside an unterminated array like one inside an unterminated object: it fails closed instead of taking the object for the verdict. Plain bracketed notes that close right away, as in prose citations (
[1],[source]), still do not block it (#1048). The judge manual (English and Turkish) describes it under "Common pitfalls".
(This cycle is a MINOR bump, not a patch. It changes the forgelm audit
exit-code contract — a critical-tier PII finding now exits 3 where it exited
0 — and changes the [distributed] extra's install behaviour on Windows.
Under docs/standards/release.md's bump table both
are MAJOR triggers, folded into a MINOR bump with an explicit ### Breaking
call-out by the pre-1.0 rule at that section's end. See below before upgrading a
pinned CI pipeline.)
forgelm auditnow exits3when the scan finds critical-tier PII — a credit-card number or IBAN. Previously it printed the finding and exited0, the same dead-gate shape the secrets scan had before v0.10.0: a real, checksum-valid card number in a training corpus was reported to an operator who had wired the step up as a gate, and the step passed. Scope is deliberately narrow. Only thecriticaltier ofPII_SEVERITYgates, and both families in it clear a checksum (issuer-prefix + Luhn for cards, ISO 7064 mod-97 for IBANs), so a hit is indistinguishable from a genuine card or account number. The card check now requires a real issuer prefix (IIN) at a length the issuer actually mints, not the Luhn checksum alone: ~9.8% of arbitrary 16-digit runs clear Luhn and every IMEI clears it by construction, so a Luhn-only gate fired on corpora of device IMEIs, order numbers or invoice references. The issuer table covers Visa, Mastercard, Amex, Discover, JCB, UnionPay, Diners Club and Mir; Maestro is deliberately omitted (its BIN range is wide enough to roughly double the false-positive rate and its common BINs already fall under Discover / UnionPay). Net random-digit-run false-positive rate ~1.1%. Government IDs, emails and phone numbers do not gate and are not intended to. The tier is what gates, not the detector:tr_idalso clears a checksum but sits athigh, so it reports without failing. Checksum validation is a precondition for gating, and most sub-critical families are shape-matched and deliberately over-report — a gate that fires on a clean corpus is a gate somebody switches off. A test pins the invariant (critical ⊆ checksum-validated), so promoting a shape-matched family tocriticalfails the suite rather than silently arming the gate on a noisy signal.--allow-piirecords the findings without failing the pipeline, mirroring--allow-secrets; the two are independent.[distributed]now carries asys_platform != 'win32'marker. DeepSpeed publishes no Windows wheels, so the unmarked requirement sentpip install forgelm[distributed]into a source build needing MSVC and the CUDA toolkit, which typically failed and took the rest of the install with it. Windows now degrades the wayqloraandunslothalready do: the install succeeds and DeepSpeed is simply absent. Reachingdistributed.strategy: deepspeedwithout it now raises a ForgeLMImportErrornaming the extra and pointing at FSDP, rather than surfacing from inside Transformers.
[tracking-mlflow]extra.training.report_to: "mlflow"has always been an accepted config value, and the field's own description told operators it needed the[tracking]extra — but that extra installswandbonly, so a run configured for MLflow died at trainer construction. MLflow now has an extra that installs it. Kept separate from[tracking]rather than folded in, so a W&B-only user does not pull MLflow's dependency tree;[tracking]keeps its historical name because renaming it would break existing installs.forgelm doctorgained a probe row for it, so an operator can confirm MLflow is installed the same way they check every other extra.tools/check_readme_links.py(29th CI guard).pyproject.tomlshipsREADME.mdas the PyPI long description with no URL rewriting, so every relative href in it resolves againstpypi.organd 404s — for exactly the reader who arrived by runningpip install forgelm. 38 links shipped that way, invisible to CI:check_anchor_resolution.pydefaults to--root docsand never opened the README, andcheck_source_path_refs.pyinspects backticked source paths rather than Markdown hrefs. The guard requires absolutehttps://hrefs on the PyPI-rendered surface and asserts that every in-repo target resolves on disk, onCONTRIBUTING.mdtoo.
- Auto-revert was documented as restoring a previous checkpoint in seven
places. It does not restore anything:
_revert_modelcallsshutil.rmtreeon the saved model directory. Corrected inforgelm/config.py(theauto_revertandmin_average_accuracyfield descriptions, which reach the generated Configuration Reference and--dry-runoutput), the audit-event catalog (EN + TR), and the user manual's configuration and audit-log pages (EN + TR). - The Unsloth "2-5× faster" figure is upstream's, not a ForgeLM measurement. It appeared across the README, six locales of the marketing site, four user-manual pages and four guides with no benchmark anywhere in the repository. Removed where a bare number was stated as fact, and attributed on the pages that keep it. Competitive-analysis tables, which describe Unsloth's own positioning, are unchanged.
- README rewritten against the code. Fourteen claims did not survive
execution — among them a Quick Start
forgelm exportcommand that exits 2 because--outputis required, auto-revert presented as an unconditional default whenevaluation.auto_revertisFalse, a Croissant card described as doubling as the Article 10 governance artefact when they are two separate artefacts, an unkeyed SHA-256 manifest sold as "proof-of-integrity", "every CLI surface has a typed entry point" when roughly half raiseAttributeError, and arevision-field list naming the tokenizer (which has no field of its own) instead of the judge model (which does).
(This cycle is a MINOR bump, not a patch.
Under docs/standards/release.md's bump table it
adds two CLI flags, five additive YAML fields, a new public exit code and new
artefact fields — none of which a PATCH ("internal refactor with no user-visible
change") may contain — and it changes two exit-code meanings, which the table
lists as a MAJOR trigger but which the pre-1.0 rule at that section's end folds
into a minor bump with an explicit ### Breaking call-out. See that section
below before upgrading a pinned CI pipeline.)
forgelm auditnow exits3instead of0when its secrets scan finds a credential. If your audit job started failing after this upgrade, this is why, and the failure is telling you the truth: the previous exit0was a bug (see### Security). The scan was always on and always printedSecrets : CRITICAL — N flagged, but the process exited0regardless, so every CI step wired up as a credential-leak gate had a gate that could not fire. Affected: any pipeline runningforgelm auditover a corpus containing AWS / GitHub / Slack / OpenAI / Google keys, JWTs, PEM private-key blocks or Azure storage keys — that step now fails. Two ways forward: scrub the corpus (forgelm ingest --secrets-mask) — the recommended one, because a credential in training data is memorised by the model and re-emitted at inference — or pass the newforgelm audit --allow-secretsto record the findings and keep exiting0. Not affected: a clean corpus (still0), and PII, quality, near-duplicate and cross-split-leakage findings, none of which gate — secrets are the only finding class that fails the command. The report is still written todata_audit_report.jsonand still printed before the process exits, so a failing run leaves complete triage material (forgelm/cli/subcommands/_audit.py,forgelm/data_audit/_summary.py).forgelm audit --output-format jsonsetssuccess: falsewhen that gate fails. A consumer asserting.success == trueover a corpus with secrets in it now seesfalse, matching the exit code — the same ruleforgelm safety-evalalready follows (successreports the gate verdict, not "the command finished"). Every pre-existing envelope key is unchanged and nothing was renamed or removed;report_pathstill points at a complete report in the failing case (forgelm/cli/subcommands/_audit.py).verify-audit/verify-annex-iv/verify-gguf/verify-integrityexit6, not1, on an integrity failure. Full detail — including which failures moved and the four that deliberately stayed on1— is under### Changedbelow. Listed here because a CI step asserting the exact code== 1as its tamper signal stops firing;!= 0,set -eand&&chains are unaffected.
-
forgelm safety-eval --max-safety-regression RATIO— the gate's threshold is now visible and settable. Float in[0.0, 1.0], default0.05, unchanged from the previous hard-wired behaviour; omitting the flag is byte-identical to before. This adds no gate. The unsafe-ratio gate always ran, but the CLI never passed a value, so every standalone run was gated atrun_safety_evaluation's signature default — a number absent from--help, from the text output and from the JSON envelope. An operator branching CI on exit3was branching on a threshold they could not read. Despite the name it is an absolute ceiling, not a baseline-relative bound: nothing measures pre-training safety anywhere. The comparison is strictly greater-than and only fires when at least one unsafe response was recorded, so--max-safety-regression 0.0still passes a clean run. Out-of-range, non-numeric andnanvalues are argparse usage errors and exit2. Not added, and not planned:--configand--classifier-revision. Nineevaluation.safety.*YAML fields and two of the three gates (min_safety_score,severity_thresholds) remain unreachable from this subcommand, and its classifier load stays unpinned. -
New public constant
forgelm.safety.DEFAULT_MAX_SAFETY_REGRESSION(0.05), exported inforgelm.safety.__all__. Single source for both the CLI flag default andrun_safety_evaluation's signature default so the two cannot drift. Deliberately not aSafetyEvalThresholdsfield — the orchestrator takes it as its own parameter and the training path still sources it fromevaluation.safety.max_safety_regression. -
forgelm safety-evaloutput echoes the threshold it gated on. The JSON envelope gains one key,max_safety_regression(additive; no key was renamed or removed — a rename would be MAJOR). The text output gains one line aftersafe_ratio. A consumer readingpassed: falsebesidesafe_ratiopreviously had no way to see which ceiling the ratio was compared against. -
Hub revision pinning — five optional config fields, all five wired.
model.revision,synthetic.teacher_revision,evaluation.safety.classifier_revision,evaluation.llm_judge.judge_model_revisionandtraining.grpo_reward_model_revisioneach accept a 40-hex HF Hub commit SHA (either case) or a branch/tag/ref, stored verbatim — ForgeLM never normalises or case-folds the value. All areOptional[str] = None, all opt-in; omitting them changes nothing. Honoured in this release:model.revisionpins every load of the base repo at the same commit —AutoTokenizer, theAutoProcessorVLM path,AutoModelForCausalLM/AutoModelForImageTextToText, and theAutoConfigprobe behind--fit-check— so the tokenizer can never drift from the weights it is recorded beside.synthetic.teacher_revisionpins the local teacher's tokenizer and weights underteacher_backend: local.evaluation.llm_judge.judge_model_revisionpins the local judge's tokenizer and weights (an API judge is loaded by the provider, not the Hub — the schema rejects the field alongsidejudge_api_key_env), andtraining.grpo_reward_model_revisionpins the GRPO reward tokenizer and sequence-classification model (rejected withoutgrpo_reward_model). For every honoured field the value is resolved to a commit SHA first and that exact SHA is passed asrevision=to everyfrom_pretrainedfor the repo, so tokenizer and model can never come from different commits. When no SHA can be confirmed — offline, nohuggingface_hub, an unreachable or gated repo — the operator's literal is still passed verbatim, so the pin is never silently dropped, and aWARNINGsays no SHA was verified. Leaving a field unset is unchanged behaviour and warns that the load is unpinned. These two are not cosmetic: the reward model is the objective GRPO optimises against, so an unpinned upstream re-tune changes what the run was trained to do; the judge's score feeds the auto-revertmin_scoregate, so an unpinned judge lets two runs of identical YAML promote and block the same model.evaluation.safety.classifier_revisionalso pins the harm classifier behind the auto-revert gate. Scope limit: the training-time safety gate only. Standaloneforgelm safety-evaltakes no--configand has no--classifier-revisionflag, so its classifier load is unpinned and logs an UNPINNED warning naming the repo; a verdict from that subcommand is not pinned evidence. Three of these five shipped dead earlier in this same dev cycle.judge_model_revision,grpo_reward_model_revisionandclassifier_revisionwere each accepted, validated, cross-field-checked and documented while reaching no loader at all. An operator who set one believed their judge scores, reward model or safety verdicts were reproducible when they were not — the same confident falsehood this work exists to remove, in a new place. The first two were wired in a follow-up commit;classifier_revisionwas missed a second time because its load site did passrevision=while neither caller — the training path nor the CLI path — passed the field down to it. Verifying a load site is not verifying the caller chain. Rejected at validation (exit1, fires under--dry-run, no network): an empty / whitespace-bearing / control-character / leading-dash / >255-char literal;judge_model_revisionwithjudge_api_key_env;teacher_revisionwithteacher_backendofapiorfile;grpo_reward_model_revisionwithoutgrpo_reward_model. Warned but accepted: a non-40-hex ref (a branch or tag is accepted and does not pin — upstream can repoint it), andmodel.revisionset against an existing local directory (warned rather than raised, because the check's verdict would otherwise depend on whether that directory exists on the validating machine).config_template.yamlships all five commented out, so no live SHA rots and the CI dry-run stays independent of Hub state (forgelm/config.py,forgelm/model.py,forgelm/fit_check.py,forgelm/synthetic.py,forgelm/safety.py). -
Base-model provenance in the Annex IV bundle.
compliance_report.json'smodel_lineageblock gainsbase_model_revision—repo_id,revision_requested(the operator's literal, so a symbolicmainshows plainly as a moving ref),revision_resolved(a confirmed 40-hex SHA ornull),resolution_source(local_path/resolved/pinned_resolved/cache/pinned_unverified/unresolved),revision_pinned(the exact string handed torevision=), andreason(present only when no base-model load happened at all — theforgelm compliance-onlycase). A non-nullrevision_resolvedalways means the load in that process was pinned to it: manifest generation performs no Hub lookup of its own, provenance is written only after the load returns, and a load that raises leaves no claim behind. Existingmodel_lineagekeys are unchanged and the addition is additive. Note that the flattenedtraining_manifest.yamlsidecar carries nomodel_lineageblock at all — it is an operator summary; readcompliance_report.jsonfor provenance (forgelm/compliance.py). -
Every pinned role reaches the Annex IV bundle —
model_lineage.component_revisions. A list sibling tobase_model_revision(which is unchanged and still present), one entry per completed pinned load in the process, sorted by(role, repo_id)so the artefact is byte-stable across runs that load the same models in a different order. Each entry carriesrole,repo_id,revision_requested,revision_resolved(a confirmed 40-hex SHA ornull— never the requested string echoed back),resolution_sourceandrevision_pinned(the exact string handed torevision=, which may be a moving ref). Six role names are artefact contract and never change:base_model,safety_classifier,llm_judge,grpo_reward_model,teacher_model,fit_check. Until this block existed, four roles resolved a revision, pinned their load to it, recorded it — and had it dropped on the floor, because the registry's only reader asked for the base model; that is what made the per-load warning's "the manifest will record …" promise false for four of the six. Two readings the artefact does not support:component_revisions: []means no pinned load completed in this process (forgelm compliance-only, an all-local-path config, a manifest written before any load) and is not a statement that no pins were configured; a nullrevision_resolvedmeans no SHA could be confirmed, not that the run was unpinned —revision_pinnedrecords the ref verbatim.fit_checkis reserved but never emitted:model.revisionis forwarded to the VRAM-estimateAutoConfigprobe, so that load is pinned, but the probe registers no provenance. Purely additive:forgelm verify-annex-ivgates on top-level Annex IV sections and does not inspectmodel_lineage, so artefacts written before this release remain valid and artefacts written after verify identically. A newly generated artefact naturally carries a differentmanifest_hashthan a pre-change build of the same run would have produced; archived artefacts keep their own self-consistent hash (forgelm/compliance.py,forgelm/model.py). -
forgelm.compliance.resolve_model_revision(repo_id, *, requested=None, offline=False)— returns{"repo_id", "revision_requested", "revision_resolved", "resolution_source"}for a model repo. Never raises.revision_resolvedis a validated 40-character commit SHA orNone; a requested pin is never echoed into it, so a value there always means something confirmed it. Withoffline=Trueit short-circuits before any Hub client is imported and consults only the commit-addressed local cache. Callers must passrevision_resolvedinto the load itself — a SHA obtained here and then not used to pin the load must not be recorded as provenance (forgelm/compliance.py). -
Per-load revision logging. One line per pinnable load:
INFOwhen a commit was confirmed (role, repo, SHA);WARNINGwhen a pin was configured but no SHA could be confirmed, stating that the load will use the requested ref as-is and naming its destination precisely — "this role's entry undermodel_lineage.component_revisionswill record that no SHA was verified rather than assert one", replacing an earlier claim about "the Annex IV manifest" that was true only for the base model;WARNINGwhen a Hub repo loads unpinned, naming the repo and the config field that would pin it. Local paths are never warned about — a directory has no Hub commit, and warning on it would train operators to ignore the warning that matters (forgelm/model.py). -
The generated model card reproduces the pin. When the base-model SHA is known the usage snippet emits
AutoModelForCausalLM.from_pretrained("org/model", revision="<sha>"); when it is not, the kwarg is omitted entirely — neverrevision="None". A card shipped inside an Annex IV bundle that tells a downstream reader to load an unpinned repo undercuts the reproducibility claim the surrounding bundle makes (forgelm/model_card.py). -
python -m forgelm(forgelm/__main__.py) — behaviour and exit codes byte-identical to theforgelmconsole script. The contributor gauntlet now uses this form, and it is not a stylistic preference: a console script'ssys.path[0]is its ownbin/directory, soforgelm --config … --dry-runvalidates whatever is installed in site-packages rather than the working tree. A stale non-editable install made that step report success against a package weeks older than the checkout. -
tools/check_cli_exit_code_prose.py— fails when a--helpstring inforgelm/cli/_parser.pyclaims an exit code the CLI does not actually return.--helpis the most authoritative place an operator reads the contract, and it was the one surface theEXIT_INTEGRITY_FAILUREchange missed: the parser still told operators that tampering exits1, and one line still described the constant as deferred to a version that had already shipped it. The existing CLI-help guard could not catch this because it validates invocation syntax, not exit-code prose. -
tools/check_import_origin.py— fails whenimport forgelmresolves outside the checkout, catching a stale or shadowing install before it can produce a false-green gauntlet run. It leads the gauntlet rather than joining the end of it: several other guards importforgelmthemselves and run withsys.path[0]set totools/, so they inherited the same blind spot — one of them validates documentation examples against the schema and would have reported OK while reading a stale one. -
forgelm doctornow reportsforgelm.install— version, resolved package directory, and whether it sits inside site-packages — as the first row, so a pasted bug report identifies which code actually ran. -
EXIT_INTEGRITY_FAILURE = 6— a new public CLI exit code for the fourverify-*subcommands (verify-audit,verify-annex-iv,verify-gguf,verify-integrity). Previously a tampered artifact and a mistyped path both exited1, so a CI pipeline could not tell an operator error from a security event. The line:6means the verifier read the target artefact and it failed its integrity check — a broken audit-log hash chain, an audit-log entry missing its_hmactag (or carrying a tag that does not recompute) underverify-audit --require-hmac, an Annex IV manifest hash mismatch, a GGUF metadata/SHA-256 sidecar mismatch, or model files that no longer matchmodel_integrity.json;1still means the verifier never got far enough to compare anything (missing path, malformed input, unreadable artefact). Four deliberate exceptions stay on1even though they look tamper-adjacent: averify-ggufmagic-header mismatch (the file isn't a GGUF at all — a file-type verdict, not a tamper verdict); averify-integritymanifest entry whose path escapes the model directory (the verifier refuses to hash an out-of-tree path before reading anything, so nothing was compared); averify-ggufmetadata-parse failure on a file whose SHA-256 sidecar matches (the bytes are provably what was exported, so the parse error is a library-version problem, not tampering); and averify-auditzero-entry log with no genesis manifest (it must fail, but with no baseline in existence the verifier cannot tell a wiped log from a mistyped path). The full rationale for each lives indocs/standards/error-handling.md(forgelm/cli/_exit_codes.py,forgelm/verify.py). -
Generation-based Llama-Guard safety scoring — the default classifier now works out of the box. The shipped default
meta-llama/Llama-Guard-3-8Bis a generative checkpoint that cannot be scored through thetext-classificationpipeline; ForgeLM now loads it as a causal LM, moderates each prompt/response pair via the Llama-Guard chat template, and parses thesafe/unsafe\nS<n>verdict (fail-closed on malformed output) into the same safety report. A newevaluation.safety.classifier_modefield (auto|classification|generation, defaultauto) selects the path —autoroutes a known generative Llama-Guard checkpoint to generation scoring and everything else to the classification pipeline. The prior fail-fast now fires only for the genuine misconfiguration (classificationmode + a generative checkpoint) (forgelm/safety.py,forgelm/config.py). -
forgelm audit --allow-secrets— record credential findings without failing the pipeline. The secrets gate that now exits3(see### Breaking) is on by default; this flag suppresses only the exit code. Detection, the printedSecrets : CRITICAL — N flaggedline, the on-diskdata_audit_report.jsonand a newWARNINGnaming the suppression all still happen, so a suppressed run is never silent. Intended for the two legitimate cases: auditing a corpus before scrubbing it withforgelm ingest --secrets-mask, and fixture sets that carry known dummy credentials. The scan itself cannot be disabled, and this is deliberately a command-line flag rather than a YAML field or an env var — suppression has to be visible in the pipeline diff a reviewer reads (forgelm/cli/_parser.py,forgelm/cli/subcommands/_audit.py). -
forgelm audit --output-format jsongains asecrets_gateblock (additive; no key renamed or removed). Keys:status("passed"— nothing critical found;"failed"— gating, process exits3;"suppressed"— findings present and--allow-secretswas passed),severity,critical_total,critical_types(a{detector: count}map) andallow_secrets. This is the only place a reviewer can tell "nothing was found" apart from "something was found and waved through" —secrets_summaryalone cannot express the difference, andsuccesscollapsespassedandsuppressedinto the sametrue(forgelm/cli/subcommands/_audit.py). -
New public helper
forgelm.data_audit.secrets_gate_verdict(secrets_summary), exported inforgelm.data_audit.__all__. The pure pass/fail classifier behind the CLI gate, for library callers that runaudit_datasetthemselves and want the same verdict without shelling out. Returns thesecrets_gatedict described above. Not added to the top-levelforgelm.__all__, so__api_version__is unchanged (forgelm/data_audit/_summary.py,forgelm/data_audit/__init__.py). -
ingest --input-encodingto read source documents in a non-UTF-8 legacy encoding, andverify-audit … --output-format jsonnow works when the flag follows the subcommand (matching the otherverify-*commands). -
A CI guard (
check_usermanual_schema_drift.py) that validates every fenced YAML key indocs/usermanuals/against the realForgeConfigschema, so a user-manual example that uses a nonexistent field is caught in CI instead of by a reader's--dry-runfailure. The widenedcheck_bilingual_code_blocksguard now also covers the user manuals. -
A CI guard (
check_release_record_sync.py) that enforces the post-release bookkeeping step. Every released version inCHANGELOG.mdmust have a matching section indocs/roadmap/releases.md— one with an actual body, since a bare heading is not a record and a(Planned)section never counts as one — and the**Released:**headline indocs/roadmap.mdand the**Yayınlandı:**headline in its Turkish mirror must both name the newest one. Release dates are cross-checked between the two files, and headings inside fenced code blocks or HTML comments are ignored so a sample snippet cannot satisfy a real release. The guard is fail-closed by construction: a heading that was plainly meant to be a release but does not parse (a missing space after##, an indented or wrong-depth heading, a unicode-dash date, a heading swallowed by an unbalanced fence) is reported by file and line rather than silently dropped, and parsing zero releases out of a CHANGELOG is treated as a broken invocation rather than a clean tree — because a guard that reports success on input it could not read is the failure it exists to prevent. The release ritual's "update the roadmap" step had been skipped for two consecutive releases —v0.8.0andv0.9.0shipped while the roadmap still announcedv0.7.0as current, and the Turkish mirror had drifted four minors behind — so it is now checked rather than trusted. The step also moved ahead of the tag indocs/standards/release.mdand thecut-releaseskill: written after the tag, the record could only redden the guard on the next PR, by which time PyPI had already shipped with a stale roadmap. -
A CI guard (
check_skill_mirror_parity.py) that keeps.claude/skills/and.agents/skills/from drifting apart. Every skill is shipped twice — once for Claude Code, once for the agent-agnostic tree — and each edit has to be applied to both copies by hand. The guard compares them after normalising the three legitimate agent-specific spellings (.claude/↔.agents/,CLAUDE.md↔AGENTS.md,Claude↔Codex) and fails on any other difference, on a skill or file present on only one side, or on a missingSKILL.md. -
A CI guard (
check_deprecation_targets.py) that keeps every deprecation removal promise honest.forgelm/config.pynow carries a singleDEPRECATION_REMOVAL_VERSIONconstant that every runtime deprecation message is built from, and the guard fails the build when any claim acrossforgelm/,config_template.yaml,docs/**ortests/**names a different version — or when the promised version is no longer ahead of the shipping version (i.e. the promise has gone retroactively false). This is the drift class that let the same removal date rot twice (v0.9.0→v0.10.0→v1.0.0); it is now mechanically impossible to reintroduce silently. -
New CI guard:
tools/check_source_path_refs.py— prose may not name a source file that does not exist. Theforgelm/safety.py→forgelm/safety/split moved the file cleanly (an AST symbol diff and a 4,100-input differential fuzz proved the runtime behaviour identical) and shipped 39 danglingforgelm/safety.pyreferences across 16 files in the very commit whose purpose was moving that file. Nothing noticed, because nothing could see them:check_anchor_resolution.pyvalidates[text](href)links underdocs/only, while the dead references lived in backticked inline paths,.claude/skills/and.agents/skills/checklists,site/*.htmlcopy, notebook JSON, andCLAUDE.md's repository-structure tree. The guard resolves everyforgelm/,tools/andtests/path named in prose against the filesystem, acrossdocs/,site/,notebooks/, both skill trees,CLAUDE.md,AGENTS.md,CONTRIBUTING.mdandREADME.md. Scope was chosen by measurement, not taste: matching every top-level directory produced 266 findings on a clean tree, skipping fenced blocks cut that to 118, and restricting the roots to the three real source trees cut it to 14 — every one a genuine defect or a documented exemption. Four false-positive controls keep it quiet enough to survive: fenced blocks are skipped (a fence showing the reader's own layout is illustrative by construction),docs/roadmap/anddocs/design/are excluded wholesale (release records and unbuilt proposals — retargeting them would falsify the record), Markdown link hrefs are stripped so a broken link is reported by exactly one guard rather than two, and individual legitimate lines live in an_EXEMPTtable that requires a written justification per entry. Wired--strictintoci.ymland the self-review gauntlet inCLAUDE.md,AGENTS.mdandCONTRIBUTING.md(now twenty-two commands).
- A guard with no chat template silently reported 100% unsafe and could
delete a good model. Generation-based scoring builds every moderation
prompt through
tokenizer.apply_chat_template. With no chat template that call raised on every pair, each failure decoded to an empty verdict, and each empty verdict scored fail-closed — so the run completed successfully reporting 100% unsafe, and withevaluation.safety.auto_reverton deleted a model that may have been perfectly fine, with nothing in the output naming the cause. Now detected once at guard load time, after the tokenizer loads and before the multi-gigabyte weight download, raising aRuntimeErrorthat names the checkpoint and both ways out (pointevaluation.safety.classifierat a real Llama-Guard checkpoint, or use a trainedsafe/unsafehead withclassifier_mode: 'classification'). Emits the existingaudit.classifier_load_failedevent (Article 15) — no new audit event, the catalog is unchanged — and exits2, since a classifier that never loaded is a runtime problem rather than a threshold failure. The check abstains rather than guessing: a tokenizer exposing neitherchat_templatenorget_chat_template, or aget_chat_templatethat fails structurally, is treated as undetermined and allowed through, so custom tokenizers are not refused on suspicion. - A safety run that could not read its verdicts no longer reports the model
as unsafe. "Unscored" is a probe pair the verifier was asked about and came
back with nothing usable on: a malformed generative Llama-Guard verdict (no
parsable
safe/unsafefirst line — including the empty string a CUDA OOM decodes to), or a crashedtext-classificationpipeline call. Each is still scored unsafe fail-closed, which is the right call per pair: a verdict you could not read is not evidence of safety, and softening it would re-open the false PASS an adversarial fine-tune earns by reliably derailing the guard into off-protocol output. What was wrong is that the aggregate merged two different facts. When at or above half the probe set is unscored, the run now reports that the evaluation could not be performed rather than that the model failed:evaluation_completed=False, which the trainer already declines to auto-revert on andforgelm safety-evalalready maps to exit2. So exit3keeps meaning "the gate said no" and2now also covers "the verifier never answered". This closed the 100%-unscored case — the misconfiguration that motivates it (classifier_mode: generationaimed at a plain chat model) has a chat template, sails past the pre-flight above, and used to land at 100% unsafe and delete a good model naming no cause. The same gate now covers theclassificationpath, whoseexceptbranch carried the identical defect (forgelm/safety/_gates.py,_orchestrator.py,_score_generation.py,_score_classification.py). - …and a run with only a handful of unreadable verdicts no longer loses its
model either. The half-the-probe-set rule above left the far more likely
case open.
evaluation.safety.max_safety_regressiondefaults to 0.05, so on a 100-probe set six malformed verdicts — an ordinary transient CUDA OOM during generation, or occasional off-protocol replies from a genuine guard — cleared the ceiling on their own, failed the run, and withauto_reverton deleted the model, under afailure_reasonreading character-for-character like a real safety failure. Two fixes, neither of which is a new constant:- Failing the gate and deleting the model now carry different burdens of
proof. Failing needs absence of evidence of safety — an unread verdict
supplies none, so the run still fails, always. Deleting needs presence of
evidence of harm — an unread verdict is not that, and auto-revert is
irreversible and runs unattended. When the same gate battery re-run over
the run's own counters with every unscored pair treated as safe would have
passed, the failure is wholly attributable to verdicts nobody could
read: the run is failed and
evaluation_completed=False, so it is kept, not reverted. This is a counterfactual over the run's existing counters, not a lowered abstain ratio — a lower ratio only moves the cliff to another arbitrary number and still hands the operator the same unreadable reason on either side of it. A genuinely unsafe model is scored unsafe in well-formed verdicts, which are scored, not unscored; its counterfactual re-run drops nothing, still fails, and auto-reverts exactly as before. If the scored rows alone breach any gate, nothing is withheld. - The failure reason now decomposes its own unsafe count. Any
failure_reasonproduced with a non-zero unscored count gains a clause naming how many pairs the classifier actually read as unsafe versus how many produced no usable verdict and were counted unsafe fail-closed. Previously six malformed verdicts and six genuinely harmful completions produced the identical sentence, so an operator's first read of a guard hiccup was "my model is unsafe".SafetyResultgainsunscored_countandscored_unsafe_count(additive; invariantscored_unsafe_count + unscored_count == unsafe_count), and the unscored warning is now logged in both scoring modes and on passing runs too — a clean run with a creeping unscored count is the early warning that the guard is degrading, previously visible nowhere. Artefact impact (both additive; no key renamed or removed).safety_results.jsongainsscored_unsafe_count,unscored_countandevaluation_completed, so a reader of the artefact alone can tell a gate failure from an unusable evaluation — previously it recordedpassed: falsefor both.safety_trend.jsonlgains the same three, because a run-over-run slide insafe_ratiodriven entirely by a risingunscored_countreads as a model getting less safe until those columns sit beside it; they are omitted for library callers still using_append_trend_entry's pre-existing four-argument signature. Thesafety.evaluation_completedaudit event payload is unchanged, sodocs/reference/audit_event_catalog.mdneeds no new row (forgelm/safety/_gates.py,_types.py,_orchestrator.py,_results.py).
- Failing the gate and deleting the model now carry different burdens of
proof. Failing needs absence of evidence of safety — an unread verdict
supplies none, so the run still fails, always. Deleting needs presence of
evidence of harm — an unread verdict is not that, and auto-revert is
irreversible and runs unattended. When the same gate battery re-run over
the run's own counters with every unscored pair treated as safe would have
passed, the failure is wholly attributable to verdicts nobody could
read: the run is failed and
forgelm verify-annex-iv --pipelineraised a tamper alarm on every clean pipeline run. If you investigated an integrity failure on a chain you had just produced yourself, found the evidence intact, and could not work out what the verifier was objecting to — this was it, and the run was clean. The orchestrator recorded each completed stage's Annex IV evidence pointer as<output_dir>/compliance/training_manifest.json, a filename no ForgeLM version has ever written:export_compliance_artifactsemitstraining_manifest.yaml(a flat operator summary, no hash) andannex_iv_metadata.json(the §1-9 canonical layout plus themetadata.manifest_hashstamp). The pointer therefore dangled on every real run, and the verifier reported the resulting absence as missing evidence. The orchestrator now records the artefact that actually carries the payload, so--pipelineexits0instead of6on a clean chain. An operator who suppressed, ignored or worked around this alarm should re-enable the check: it now means what it says (forgelm/cli/_pipeline.py,forgelm/verify.py).- Archived pre-0.9.1 pipeline manifests still verify. A pointer naming the
legacy
training_manifest.jsonresolves to itsannex_iv_metadata.jsonsibling, but only when the chain manifest'sforgelm_versionparses to a release earlier than0.9.1. On a current manifest — or one whoseforgelm_versionis absent or unparseable — the fallback does not apply and an absent target routes conservatively to a violation. Only the leading numeric release components are compared, so a pre-release such as0.9.1rc1counts as0.9.1and does not unlock the compatibility path (forgelm/verify.py). - The chain verifier no longer dies with a raw traceback on a deeply-nested
document. A pipeline manifest or per-stage artefact of ~100 KB — about
twenty times under the 8 MiB byte cap — nested deeply enough to exhaust the
interpreter stack killed
verify-annex-ivinsidejson.loadwith no stdout and no JSON envelope, becauseRecursionErroris neither anOSErrornor aValueErrorand escaped every existing handler. Depth, not byte count, is what exhausts the stack. Both sites now refuse the document instead of crashing: a nested per-stage artefact is a violation (exit6), a nested chain manifest is an input error (exit1, refused unread) (forgelm/verify.py). - The chain manifest is now size-capped, like the per-stage artefact already
was.
pipeline_manifest.jsonwasjson.loaded with no cap at all, directly contradicting the rationale written for the stage-level cap: a 600 MB manifest reaches roughly 3.6 GB peak RSS.PIPELINE_MANIFEST_MAX_BYTESis deliberately the same 8 MiB as the stage cap so there is one number to reason about — orders of magnitude above any legitimate manifest (one row per stage, single-digit kB) and far below a size that exhausts memory. It routes to exit1, not6, because the file is refused unread: nothing was compared (forgelm/verify.py). - The documented symlink refusal for relative evidence pointers was dead
code.
os.path.realpathran on the joined pointer before anything inspected it, soos.path.islinkcould never be true and a relative pointer aimed at a symlink was silently followed. The relative branch now runs three checks that all bite: lexical containment rejects../escapes without resolving anything, the symlink refusal inspects the unresolved path, and a final realpath containment check catches an escape smuggled through a symlinked parent directory. Absolute pointers remain allowed unconditionally (a stage'straining.output_dirlegitimately lives outside the pipeline tree) and are still refused when they are symlinks or directories (forgelm/verify.py). - Hub metadata lookups had no timeout at all and could hang a run
indefinitely.
HfApi().model_info()/.dataset_info()default totimeout=None, which is not a sensible library default but no ceiling: a firewall that drops packets silently, or a hijacked DNS answer, blocked the caller forever. Revision resolution now runs on every online load whether or not a pin is configured, and it runs before training starts — including on a fully-cached machine where the load itself would have needed no network — so an unbounded metadata call converted a run that used to work offline-by-accident into one that never began. Every such call is now bounded at 10 seconds (forgelm.model.HUB_API_TIMEOUT_SECONDS). On timeout the run continues and the provenance record degrades tounresolved; it never fails the run, because a provenance helper that can abort a fourteen-hour training job is the worse trade (forgelm/model.py,forgelm/compliance.py,forgelm/data.py). TRANSFORMERS_OFFLINEsuppressed dataset lookups but not model-revision lookups. The model side's offline env-var tuple omitted it while the data side included it, from a comment claiming the two were the same list — so an operator who air-gapped a box withTRANSFORMERS_OFFLINE=1alone got no dataset lookup (correct) and a full round of model-revision lookups (wrong). All three ofHF_HUB_OFFLINE/HF_DATASETS_OFFLINE/TRANSFORMERS_OFFLINE— likemodel.offline: true— now mean no Hub request is attempted on either side (forgelm/model.py).- The Annex IV dataset revision SHA was decoupled from the load that
produced it, and could name a corpus the run never read.
compliance._fingerprint_hf_revisionobtainedhf_revisionby callingHfApi().dataset_info(path)at manifest-generation time, with no coupling of any kind todata._load_single_dataset'sload_dataset(path). What it recorded was the repo's default-branch head when the manifest was written, not the commit that was trained on — while its own docstring called that SHA "the only stable identifier that lets Article 10 reviewers reproduce the exact corpus", and the ISO/SOC 2 mapping pages cited it as input-traceability evidence. Whenever the upstream repo moved between the load and the manifest — precisely the case provenance exists to detect — the artefact asserted a falsehood with full confidence. A missing pin is honest; a wrong pin is not. ForgeLM now resolves a Hub dataset's commit SHA first, passes that exact SHA toload_dataset(..., revision=...), and records it only after the load returns. The fingerprint gainshf_revision_source, now written on every dataset fingerprint (previously only on the Hub branch, so its absence was ambiguous between an old artefact and a local corpus). Four mutually exclusive values:loaded(the SHA the load was pinned to; the only value an auditor may treat as evidence of what was trained on),unverified(a manifest-time Hub lookup tied to no load — whatforgelm compliance-onlyproduces, since it writes a bundle without reading the corpus; a lead, not proof, and it names a commit the run never read if upstream moved in between),local_path(the corpus is files on disk, so no Hub commit exists and none was sought — no reason is written because nothing failed; for a local file the evidence is thesha256content hash, for a local directory there is no content hash and the record identifies the path only), andunresolved(a lookup was attempted or refused;hf_revisionis absent andhf_revision_reasonstates why, truncated to 200 chars).loadedis evidence,unverifiedis a lead,local_pathandunresolvedare honest gaps — and a gap is never grounds to infer a revision.hf_revisionkeeps its name, type and absent-when-unknown semantics, so tooling that reads only that key is unaffected — but it should now checkhf_revision_source == "loaded"first.hf_revisionnever holds a branch name, tag or moving ref: theunverifiedbranch previously skipped the commit-shape check, so a Hub client answering with a symbolic ref could put the literal"main"into a field auditors read as a commit. A non-commit answer is now refused and recorded asunresolvedwith the rejected value quoted in the reason. Operators holding bundles produced before this release: those manifests carry a bare, unlabelledhf_revisionobtained the old way. Treat it asunverified. Nothing back-fills a SHA that was never captured, and the append-only principle forbids rewriting an existing manifest, so this fix makes future runs auditable — not past ones (forgelm/compliance.py,forgelm/data.py). - A local-directory corpus is no longer fingerprinted as a Hub dataset.
compute_dataset_fingerprinthad two branches — "is a file" and "everything else is the Hub" — so a directory of JSONL files (a documenteddata.dataset_name_or_pathform) and a typo'd path were both labelledsource: huggingface_hubwith adataset_id, sent to the Hub, and written down as a failed lookup rather than as files with no Hub identity. Routing is now explicit: a local file (unchanged — nosourcekey,sha256content hash),source: local_directoryfor a directory (nodataset_id, andresolved_pathwhen the path is a symlink),source: huggingface_hubfor a Hub-id-shaped path, andsource: unknownfor a path that is neither on disk nor Hub-id-shaped — recorded asunresolvedwith that stated as the reason and no Hub request made on its behalf. Downstream consumers that assumed "not a file ⇒ Hub" need updating (forgelm/compliance.py). model.offline: truenow suppresses Hub traffic on the data and provenance path by argument passing rather than environment side-effect.compute_dataset_fingerprint,_fingerprint_hf_revision,_resolve_hub_dataset_revision,_load_single_datasetand_merge_extra_datasetseach take anofflineflag, andprepare_dataset/generate_training_manifestderive it from the config. Previously the only protection was the CLI exportingHF_HUB_OFFLINEat start-up, so a library consumer who setmodel.offline: trueand called into these modules directly got outbound connection attempts and no warning. The dataset-metadata fetch (version,description,download_size_bytes) is now skipped in offline mode as well, so those keys are absent from an air-gapped manifest rather than absent only after a failed network attempt — attempting is the thing an air-gapped deployment is asking us not to do. The CLI's env-var export is unchanged and still works as a fallback, andTRANSFORMERS_OFFLINEis now honoured byforgelm.dataalongsideHF_HUB_OFFLINEandHF_DATASETS_OFFLINE(forgelm/data.py,forgelm/compliance.py).mix_ratiolength is validated before any dataset is loaded rather than after. SameValueError, same message, strictly earlier — the count is known from the arguments alone, so downloading the datasets a mixture rejects was pure waste (forgelm/data.py).- GRPO training no longer crashes at post-train evaluation.
ForgeTrainercalledself.trainer.evaluate()(and measured a baseline loss) on aGRPOTrainerthat is intentionally built with noeval_dataset, so every GRPO run with a validation split — the default, including the bundledgrpo-mathquickstart — aborted withEXIT_TRAINING_ERROR. Both call sites are now gated ontrainer_type == "grpo", matching the rationale already applied in the GRPO branch of the training-args builder (forgelm/trainer.py). - GDPR
purge --row-idsurvives a non-UTF-8 corpus. A corpus containing invalid UTF-8 bytes previously raised an uncaughtUnicodeDecodeErrorafter thedata.erasure_requestedaudit event was written, leaving a dangling audit chain with no closing record. The read/rewrite paths now catch(OSError, UnicodeDecodeError)uniformly, emitdata.erasure_failedwith a sanitised message, clean up the temp file, and exit with the documented training-error code — the append-only chain always closes (EU AI Act Art. 12). - Reverse-PII audit
error_messageis now PII/secret-masked and the raw-PII persistence warning is emitted in JSON output mode too, matching text mode. - Unsloth model loading forwards
trust_remote_code, and the GRPO classifier reward model loads at the resolved bf16/fp16 compute dtype instead of fp32 (forgelm/model.py,forgelm/trainer.py). AuditLoggerno longer crashes when the OS user has no passwd entry. The operator-identity fallback caught onlyOSError;getpass.getuser()raisesKeyErrorin a container with no/etc/passwdentry (andImportErroron Windows withoutpwd). Both are now handled, falling through to the anonymous-opt-in policy instead of aborting (forgelm/compliance.py).- The audit genesis manifest is written atomically and verified fail-closed.
A corrupt/unreadable genesis manifest was previously tolerated at write time,
silently defeating the tamper-evident chain; it now raises (gated by the same
FORGELM_ALLOW_AUDIT_REROOT=1opt-in as the absent-log path) and the manifest write istmp + fsync + os.replaceatomic (forgelm/compliance.py). - A non-numeric LLM-judge score no longer crashes the whole evaluation. A
valid-JSON verdict like
{"score": "8/10"}now degrades to the documentedNonesentinel with a warning instead of raising (forgelm/judge.py). fit-checkVRAM estimates read the checkpoint's declared dtype. The quant-scheme fallback assumed bf16 for non-4bit runs; it now honours the model's native dtype (bf16/fp16/fp32) so a bf16 checkpoint is no longer double-counted and an fp32 checkpoint no longer produces a false "fits" (forgelm/fit_check.py).- DARE adapter merges are reproducible run-to-run. The per-key drop mask
seed used Python's process-randomized
hash(); it now uses a stablehashlib-based hash (forgelm/merging.py). - TIES disjoint-merge renormalizes by the sign-agreeing weight sum, so
merged weights are a proper average instead of being silently shrunk toward
zero (
forgelm/merging.py). - Data-audit quality score no longer undercounts.
overall_quality_scorewas silently wrong on a multi-split corpus containing a zero-flag split; evaluated-sample counts now always reach the denominator. Single-half instruction/response pairs are now scanned for PII/secrets, and the IBAN detector matches the ISO 13616 spaced print form (forgelm/data_audit/). forgelm ingestno longer leaves a torn output file on a multi-file abort. The JSONL is streamed to a temp file and atomically promoted onto--outputonly after the whole corpus succeeds; a mid-run failure cleans up and exits with the training-error code. Thefrontmatter_pages_droppedmetric no longer undercounts across a multi-file PDF corpus (forgelm/ingestion.py).- The human-approval gate is concurrency-safe and container-safe.
approve/rejectnow serialise their read-check-write under a file lock (no approve-vs-reject race), and anAuditLoggeridentity failure exits with the documented config-error code instead of an uncaught traceback (forgelm/cli/subcommands/_approve.py). - Pipeline errors honour the JSON output contract. Rejecting a pipeline-only
flag on a single-stage config, and a config re-read failure, now emit the
structured JSON error envelope instead of bare text; stale per-stage metrics
no longer survive a crashed resume attempt into the Annex IV manifest
(
forgelm/cli/_pipeline.py,forgelm/cli/_dispatch.py). - The audit/verification toolbelt no longer crashes on a corrupt log.
iter_audit_eventsand theverify-annex-iv/verify-gguf/verify-integritycommands surface a controlled integrity error on a non-UTF-8 artefact instead of an uncaught traceback, andverify-auditsupports JSON output (forgelm/cli/subcommands/). - Webhook delivery keeps its fire-and-forget contract. A missing optional
dependency, a non-
RequestExceptiontransport error, and a mixed-caseHTTP://URL are all handled without failing an otherwise-successful run;deploynarrowed its exception handling so serialization bugs surface instead of being masked (forgelm/webhook.py,forgelm/deploy.py). cache-modelsreports the real on-disk size. The size walk skipped the HF snapshot symlinks instead of following them into the blob store, so every cached model showed a near-zero size on POSIX; it now resolves and de-duplicates blob targets (forgelm/cli/subcommands/_cache.py).- GGUF export surfaces the K-quant two-step as structured data. When a
requested K-quant (e.g.
q4_k_m) is produced asf16pending a manualllama-quantizestep, the result now carriesrequested_quant,manual_step_required, andfollowup_command(also in the--output-format jsonenvelope) instead of only a log line; the output parent directory is created if missing and the converter timeout is configurable (forgelm/export.py,forgelm/cli/subcommands/_export.py). - The config wizard is more robust.
_save_config_to_filenow catchesyaml.YAMLError/serialization errors (not onlyOSError); a declinedrope_scalingprompt recomputes the factor fresh instead of reusing a stale one; a strict-tier safety-eval override prints an in-context Article 9 notice; and the HF-Hub id / webhook-preflight paths are hardened (forgelm/wizard/). - Mutually-exclusive non-training mode flags. Passing two of
--dry-run/--fit-check/--benchmark-only/--merge/--generate-data/--compliance-exportis now rejected by argparse instead of silently running only the first;Ctrl-Cduring the interactive wizard exits with the wizard-cancelled code (5) instead of a traceback (forgelm/cli/). - An unsupported dataset file extension fails fast.
data.dataset_name_or_pathpointing at, e.g., a.txtfile now raises an actionable config error listing the supported formats instead of deferring to an opaqueload_dataseterror (and, offline, a network lookup). A nativetestsplit is moved (not aliased) tovalidation, avoiding a redundant tokenize pass; synthetic generation flushes incrementally so a mid-run crash keeps completed rows (forgelm/data.py,forgelm/synthetic.py). - Archive helpers clean up on failure and pin encodings.
manage_checkpointsnow catchestarfile.TarError(not onlyOSError) so a partial archive is removed, writes asha256sum-compatible sidecar, and the HF-token file is read as UTF-8; the public registries are now immutable mappings (forgelm/utils.py,forgelm/__init__.py). - CI hardening. Every CI job now declares least-privilege
permissions: contents: read; the Docker Compose example config path matches the real mount and the TensorBoard image is pinned; the MinHash-LSH dedup backend is exercised against the realdatasketchlibrary in CI (.github/workflows/,docker-compose.yaml).
-
The safety gate could return PASS for a model it had scored wrong — a misconfigured classifier scored every response SAFE. Filed here rather than under
Fixedfor the same reason as the Annex IV entries below: the defect did not mute a safety signal, it reversed one. Generation-mode verdict parsing accepted any first line beginning withsafe, so a checkpoint that is not a guard at all — one replyingSAFETY: this is harmfulorSafety concerns apply here— cleared the gate. The gate passed, auto-revert did nothing, the model shipped, and the compliance artefact recorded that safety had been checked. If you ranevaluation.safetywithclassifier_mode: generationpointed at anything other than a real Llama-Guard checkpoint, re-run the evaluation — a PASS from before this release is not evidence of anything. Asafeverdict now requires the whole first line to besafe(case-insensitive, a trailing.or!tolerated); anything else is malformed, scored unsafe fail-closed, and flaggedlow_confidencefor review. Theunsafeside stays lenient on purpose (first word only) — leniency there cannot produce the mirror-image bug, and it keeps the legitimate single-lineunsafe S5form routed to category extraction instead of dropping its S-code from the report. Genuine Llama-Guard output is unchanged:safe,unsafe,unsafe\nS1,S5,unsafe S5, whitespace and case variants all behave exactly as before. Operator impact: a safety report that previously passed against a misconfigured classifier may now fail — that is the fix, not a regression; the old result was a false PASS. One narrowing to know about: trailing decode noise is tolerated only on a subsequent line, sosafe </s>orsafe,on the verdict line itself now lands in thelow_confidencebucket. The twoFixedentries on unscored verdicts describe the follow-on work that keeps this fix from deleting good models. -
The
forgelm auditcredential-leak gate had never fired. The secrets scan detected AWS / GitHub / Slack / OpenAI / Google keys, JWTs, PEM private-key blocks and Azure storage keys correctly, printedSecrets : CRITICAL — N flagged, wrote them todata_audit_report.json— and then exited0. A CI step wired up per the documentation (docs/usermanuals/en/data/secrets.mdpromised "acriticalseverity exits non-zero so a CI pipeline fails fast") therefore reported success while its own output said otherwise: a control that failed open, which is worse than no control, because the operator believed they were covered. If you have been relying onforgelm auditas a credential gate, treat every prior green run as unverified and re-audit the corpora that fed your released models. The gate now exits3; see### Breakingfor the migration and--allow-secretsfor the escape hatch (forgelm/cli/subcommands/_audit.py). -
Webhook
**extrapayload keys are now allowlisted._send(**extra)forwarded whatever a caller passed straight into the JSON body delivered to a third-party Slack/Teams endpoint._ALLOWED_EXTRA_PAYLOAD_KEYSnarrows it to exactly the four keys the shippednotify_*methods already pass. This is a true non-change for receivers — no field that used to arrive stops arriving and no payload changes shape — and is preventive: it closes**extraas a future route for user- or config-derived text to reach an external receiver (forgelm/webhook.py). -
A stage could be dropped from
--pipelineverification entirely by downgrading its status, and the report never mentioned it. The chain verifier deep-parsed only stages whosestatuswas exactlycompletedand silently skipped every other row. Becausemetadata.manifest_hashis an unkeyed digest computed by the publiccompute_annex_iv_manifest_hash, anyone able to write the archive could delete a stage'sannex_iv_metadata.json, flip that stage's status, recompute the digest and obtain a clean report: reproduced on a two-stage chain, where the tampered stage vanished from the output withhash_statestillverified. The single-stage case was already caught by thefinal_status: completedwith zero completed stages rule; the multi-stage case was not. Three reader-side rules now narrow it — every stage row is published through the newstages_total,status_censusandstage_dispositionsenvelope fields so none can be silently omitted; a status token outside the closed set of seven any ForgeLM version writes is a violation (exit6) rather than a skip; andgate_decision: "passed"alongside a non-completedstatus is a violation, since that gate value is written only next tostatus = "completed". Not fully closed, by construction: a stage that completed without agate_decisioncan still be downgraded without producing a violation, and no reader-side check can fix that while the manifest is unauthenticated — the remaining defence is assertingstages_examinedagainst the stage count the pipeline config declares. Threat model and residual gap documented indocs/reference/verify_annex_iv_subcommand.md(+ TR mirror) (forgelm/verify.py). -
Deleting a stage's Annex IV evidence routed softer than corrupting it — the Article 12 tamper signal was inverted. Filed under Security rather than Fixed because the defect did not merely mute a compliance signal, it reversed it, and the operator-facing message actively argued the auditor out of the correct conclusion. With the orchestrator emitting a pointer no writer satisfies (see Fixed), the reader could not distinguish "the writer never produced this file" from "someone removed it", so the two were collapsed onto the softer verdict. Measured on the shipped build:
evidence present but rotten -> EVIDENCE_VIOLATION -> exit 6 evidence DELETED entirely -> EVIDENCE_UNVERIFIED -> exit 1Deleting evidence is the archetypal Article 12 tampering and is more severe than corrupting it, yet it drew the lesser code — and the exit-
1message told the auditor in prose that the cause was "a writer defect, not tampering", an assertion the verifier had no basis to make. That claim is now deleted from every operator-facing message; it survives only as a source comment recording the history.Missing evidence is now routed on what the run actually configured, which the verifier can establish and the manifest hash protects:
- The chain manifest carries a populated
annex_ivblock, so the artefact was written and is now gone → violation, exit6. - The run configured no
compliance:block (noannex_ivkey, or all three §1 identity fields blank), so nothing was ever produced → unverified, exit1, and the message never phrases this as a deletion.
This restores the governing rule shipped earlier in this cycle:
6= the verifier compared something and it did not match;1= it never got to compare anything;2= runtime/IO. Exit-code precedence was reordered to honour it (forgelm/verify.py,forgelm/cli/_pipeline.py). - The chain manifest carries a populated
-
Un-loadable default safety classifier now fails fast and is audited. The shipped default
meta-llama/Llama-Guard-3-8Bis a generative checkpoint with no trained sequence-classification head and can never score through thetext-classificationpipeline. It is now refused at evaluation start with an actionable error (instead of crashing deep in the stack after a multi-GB download), and the rejection is recorded as anaudit.classifier_load_failedArticle 15 event on both the fail-fast and load-failure paths (forgelm/safety.py). -
auth.hf_tokenandsynthetic.api_keyare now redacted from root-level config dumps and hashes. The per-field redaction was dead code once nested underForgeConfig(Pydantic v2 does not invoke a nested model'smodel_dump()override), so a rootmodel_dump()/model_dump_json()— andcompute_config_hash— leaked the raw secret into serialised manifests. RootForgeConfigmodel_dump()andmodel_dump_json()overrides now mask both paths while keeping attribute access as plain strings for internal consumers (forgelm/config.py). -
Build toolchain moved off a vulnerable
setuptools. The build-system floor is nowsetuptools>=83.0.0— the first release carrying the fix for PYSEC-2026-3447 — and the nightly supply-chain job upgrades the scanned environment to match. The nightlypip-auditgate had been failing closed on this advisory since 2026-07-15 (#69); it is now fixed rather than suppressed (no entry was added totools/pip_audit_ignores.yaml). -
ReDoS hardening in the OpenSSH/PGP private-key secret detectors. The unbounded
.*?key-body match underDOTALLis now length-bounded, removing a quadratic blow-up on operator-controlled corpus lines (forgelm/data_audit/_secrets.py). -
The SSRF guard now blocks RFC 6598 Shared Address Space (100.64.0.0/10). That range was neither private nor reserved to Python's
ipaddress, so a config-controlled URL (webhook.url,judge.judge_api_base,synthetic.api_base) could reach a cloud metadata endpoint inside it — notably Alibaba Cloud's IMDS at100.100.100.200. The single_is_blocked_ipchokepoint now rejects it (including IPv4-mapped IPv6), and non-finite (nan/inf) request timeouts are rejected (forgelm/_http.py).
- The
lora.use_dora/lora.use_rslora/training.sample_packingremoval target is nowv1.0.0. These are YAML schema fields, anddocs/standards/release.mdclassifies "changed YAML schema with removed/renamed fields" as a MAJOR change — so the earlier targets (v0.9.0, thenv0.10.0) promised a breaking removal inside a MINOR release, a promise no minor could legitimately keep. That mismatch is why the deadline slipped:v0.9.0shipped with the aliases still present, and the reference docs still advertisedsample_packingas "removed in v0.9.0" until now. Re-targeting to the next MAJOR makes the deprecation contract consistent with the project's own versioning policy; the aliases keep forwarding with aDeprecationWarninguntil then.
-
forgelm/safety.pysplit into theforgelm/safety/sub-package. The module reached 1038 LOC — past the ~1000-LOC sub-package trigger indocs/standards/architecture.md— once generation-based Llama-Guard scoring landed. It is now_types,_inputs,_generate,_classifier,_score_classification,_score_generation,_gates,_resultsand_orchestratorbehind a re-exporting__init__, following the existingforgelm/cli/,forgelm/data_audit/andforgelm/wizard/precedent. This is a move, not a rewrite: no behaviour changed.run_safety_evaluationandSafetyEvalThresholdskeep their public import paths, sofrom forgelm.safety import ...andfrom forgelm import ...are both unaffected. -
The four
verify-*implementations moved into a newforgelm/verify.py.verify_gguf,verify_integrity,verify_annex_iv_artifactand their result dataclasses previously lived inside theforgelm.cli.subcommands._verify_*modules, where library callers were importing them out of a CLI package. The logic now sits in one importable module and the subcommands are thin wrappers. This is a move, not a rewrite (the behaviour changes in this cycle are recorded separately in this section and under### Security), and it is backward-compatible: both the old and the new import paths resolve, and the lazily-exported names onforgelmitself are unchanged, so__api_version__does not move. Announced for the same reason as theforgelm/cli/,forgelm/data_audit/andforgelm/safety/splits — anyone who pinned a private path gets to see it here rather than discover it (forgelm/verify.py). -
Module-size deferrals now carry an LOC budget instead of a target version, and growing past that budget fails the build. Every entry in
tools/check_module_size.pywas labelled "defer to v0.6.x split" — accurate when written at v0.5.5, still being printed at v0.9.1, three minor releases after that cycle closed. Because the label was advisory, deferred modules were free to grow while the guard emitted an unchanged WARN;forgelm/compliance.pywent from ~1500 to 2147 LOC over that span. Each of the seven remaining deferrals is now pinned to its measured LOC, and exceeding it is fatal in every mode. Deferrals name no version at all — a budget makes no prediction and so cannot go stale. Raising a budget is the explicit escape hatch: edit the literal in the same PR and record why in the entry'sbudget_history, so extra headroom is always a reviewed diff line. The guard also now reports a dangling entry (path no longer exists) as fatal, and a stale entry (module back under the ceiling, with hysteresis so it cannot flap) as fatal under--strict.forgelm/cli/subcommands/_doctor.pyleft the list on measurement — at 950 LOC it had been under the ceiling for some time while still being reported as debt. The ordered backlog, the per-module rationale, and a plain statement that these seven splits are not currently scheduled are recorded indocs/roadmap/risks-and-decisions.md. -
compute_config_hashvalues shift for every existing config, purely because the five newrevisionfields now exist withNonedefaults and are therefore part of the canonical serialisation. No behaviour changed and no YAML needs editing — but an auditor diffing a pre- and post-upgradeconfig_hashof what is byte-identical YAML will see a mismatch and file a finding unless they know why. This is stated here so that conversation starts from the answer. -
Hub loads for models and datasets now emit a
WARNINGwhen they are unpinned. Existing configs are unaffected functionally, but a run that previously logged nothing now names each Hub repo loading from a movable default branch. Set the matchingrevisionfield to silence it — this is the intended remedy, not log noise to filter. -
Known remaining gaps in revision pinning, stated so a reviewer does not have to discover them:
forgelm cache-modelshas no--revisionflag, so an air-gapped staging run fetches the default branch and a pinned run on the disconnected host will miss its snapshot;--dry-runreports no pin status and (by its documented contract) never checks that pins are fetchable, so a pipeline running--dry-runbut notforgelm doctorcan get a green validation followed by a load-time failure; merge-source models (merge.models[]) cannot be pinned; andexport/inference/mergingare unpinned by design because they load local artefacts this run produced. -
verify-audit/verify-annex-iv/verify-gguf/verify-integritynow exit6, not1, when the target artefact was read and failed its integrity check (broken audit-log hash chain, Annex IV manifest hash mismatch, GGUF metadata/SHA-256 sidecar mismatch, or model files that no longer matchmodel_integrity.json). Also moved: theverify-audit --require-hmacfailures — a log whose chain is intact but whose entries carry no_hmactag, or a tag that does not recompute under the secret in$FORGELM_AUDIT_SECRET, exited1and now exits6. This is called out separately because it is not a chain break, so a reader scanning the list above will not find it there, and because--require-hmacexists precisely for the regulated CI pipelines most likely to assert an exact code. (The usage error —--require-hmacwith the secret env var unset — is unchanged at1: nothing was compared.) Affected: a CI pipeline step that asserts the exact exit code== 1to catch averify-*tamper signal — that assertion needs== 6added alongside it. Not affected: a pipeline that only branches on!= 0, or that runsverify-*underset -e/&&chains — both1and6remain non-zero and still fail the step the same way. A caller/input error on the same four subcommands (bad path, malformed JSON, magic-header mismatch onverify-gguf) is unaffected and still exits1(forgelm/cli/subcommands/_verify_audit.py,forgelm/cli/subcommands/_verify_annex_iv.py,forgelm/cli/subcommands/_verify_gguf.py,forgelm/cli/subcommands/_verify_integrity.py). -
forgelm verify-auditno longer reports success on a log with zero entries. It previously printedOK: 0 entries verifiedand exited0— the code a CI pipeline reads as "the Article 12 record is intact" — after comparing nothing at all. An empty log is never a legitimate fresh-run state:AuditLoggercreates its output directory but not the log file, and the file and its genesis manifest are both written by the first event, so a never-used log is absent (still exit1,audit log not found), not empty. An existing empty log is therefore a truncation, a rotation that moved the body away, a straytouch, or a wrong path. The verdict splits on whether a baseline survived: with no genesis manifest the command now exits1(nothing existed to compare zero entries against — the verifier cannot distinguish a wiped log from a mistyped path, and crying tamper on atouched file would be the mirror image of theverify-ggufmagic-header judgement call); with a manifest that pins a first entry, or one that is present but corrupt, it exits6as it already did (a baseline existed, so the comparison ran and failed). Affected: any caller that treated exit0on an empty log as a pass — a CI gate over a log that was rotated or truncated between the training step and the verify step now fails where it used to go green, which is the point; andforgelm.verify_audit_log()now returnsvalid=False, entries_count=0there instead ofvalid=True. Not affected: every log with at least one entry — clean, tampered, or unreadable — keeps its existing exit code exactly, andVerifyResult's public fields and the JSON envelope shape are both unchanged (successis simplyfalsewhere it used to betrue) (forgelm/compliance.py,forgelm/cli/subcommands/_verify_audit.py). -
forgelm verify-integrityno longer reports success on a manifest that attests to nothing. Amodel_integrity.jsonwhoseartifactslist is empty, that has noartifactskey at all, whose JSON root is not an object, or that contains a non-object artifact entry now exits1with a message naming the defect, instead of hashing zero files and reporting a pass. These are input errors rather than integrity failures (1, not6): nothing was ever hashed, so there is no integrity verdict to report — the same "never got to compare anything" line the otherverify-*subcommands draw (forgelm/verify.py,forgelm/cli/subcommands/_verify_integrity.py). -
rope_scalingis validated at config load. A malformedtype/factorpayload now fails fast with a config error (exit 1) instead of surfacing as a runtime crash mid-training (forgelm/config.py).
-
docs/reference/safety_eval_subcommand.md(+ TR) corrected against the gate's new behaviour. Two claims went stale the moment the unscored attribution landed, both of them the kind a CI author codes against. The exit-code table described2as load-time failures only, when anyevaluation_completed=Falseresult now routes there — including the two abstentions decided after scoring; and the JSON-envelope section statedfailure_reason"is one of three fixed formats", which a prepended abstention reason and an appended decomposition clause both break. The envelope note now tells consumers to branch onpassedandevaluation_completedrather than pattern-match the prose. A new "Unscored probes and the two abstentions" section (+ TR mirror) defines what unscored means, why it is counted unsafe fail-closed, and why neither abstention can ever promote a model. -
evaluation.safety.include_eval_samplesnow says what it actually exposes. All of its documentation surfaces advertised onlypromptandresponse, silent onraw_verdict— the generative guard's own output, truncated to 200 characters — which was added to the same redaction set alongside generation-based scoring and is therefore written to disk by the same switch. That matters because a misconfigured guard echoes or continues the adversarial probe rather than answering it, so the field can carry precisely the content the other two exist to strip; an operator turning on a debugging flag has to know that. Corrected indocs/reference/configuration.md(+ TR),docs/usermanuals/{en,tr}/evaluation/safety.md(prose + parameter table) anddocs/usermanuals/{en,tr}/reference/configuration.md, and noted against the original redaction entry indocs/roadmap/releases.md. -
docs/reference/audit_event_catalog-tr.mdre-synced with the EN sibling. Structurally mirrored but false in meaning on three counts — the fourth such TR meaning-drift this cycle, and like the previous three the parity guard was green throughout because it compares heading spines, not claims. The TR page still told a Turkish reader that the guard's hardcoded_EVENT_NAMESPACESlist is what hidesquickstart_audit.jsonl(that list has been deleted; theevent_typekey is now the only thing hiding it), still describedclias a namespace the guard recognises, and omitted the entiresafety_trend.jsonlsubsection — the second non-Article-12 log, whose whole point is that it looks like an audit trail and is not one. -
Phase 14.5 Task 5's closure is now consistent across all five places that record it.
docs/roadmap/phase-14-5-pipeline-hardening.mdanddocs/roadmap/risks-and-decisions.mdrecorded it NOT SCHEDULED; on the same daydocs/roadmap.md, its TR mirror anddocs/roadmap/releases.md's unreleased v0.9.x section all still said it "stays open" / "still open". A reader arriving from the public roadmap was told the opposite of the decision. All five now state the closure and its condition-gated revisit criterion. No guard reconciles a status duplicated across five documents — this is the same hand-maintenance shapecheck_deprecation_targets.pyandcheck_release_record_sync.pywere each built to close after it rotted twice. -
What was investigated and deliberately not built is now on the record. Three items were swept as undelivered promises and closed as decisions rather than left to rot as open ones; each is written with a revisit condition instead of a version, because a version is a prediction that can quietly become false. (1) Phase 14.5 Task 5 (SonarCloud S3776 complexity refactor) is rewritten in
docs/roadmap/phase-14-5-pipeline-hardening.mdas not scheduled: re-measurement found its counts, function list, file:line references and acceptance criterion all wrong — it named six breaching functions where an in-repo AST approximation finds ~46, omitted the two worst (ingest_path~73,verify_integrity~37), listed one that no longer breaches, and set its success criterion against a SonarCloud scan that no workflow in this repo runs. (2) The GRPO reward-model 4-bit knob is recorded as a phantom — never promised in the public tree, explicitly deferred in favour of the dtype fix that shipped, and rejected on merit besides, since quantising the reward model perturbs the scale GRPO optimises against. A reward-model memory-footprint note landed indocs/guides/alignment.md(+ TR). (3)forgelm quickstart's audit trail is documented as a deliberately unchained convenience log, not part of the Article 12 chain, indocs/reference/audit_event_catalog.md(+ TR) — together with the fact thattools/check_audit_event_catalog.pystructurally cannot see the file and reports green having never examined it, so a passing guard is not mistaken for coverage there. -
docs/reference/safety_eval_subcommand.md(+ TR) documents the new--max-safety-regressionflag, the echoed threshold in both output formats, the chat-template pre-flight and its exit-2routing, and — under a new heading — exactly which safety gates and YAML fields remain unreachable from the standalone subcommand, so the gap is not rediscovered as a surprise. -
docs/reference/webhook_schema.md(+ TR mirror) — the canonical webhook contract, which did not previously exist.v0.7.0shipped threepipeline.*events on top of the pre-existing five-event single-stage vocabulary with no single reference enumerating the surface; a receiver author had to readforgelm/webhook.pyor reconstruct the list from CHANGELOG. The new page documents all eight webhook events, when each fires and whichnotify_on_*flag gates it, the seven always-present payload keys with types, the four event-specific keys and which event carries each, a worked example payload per event family, the transport and SSRF/TLS policy, the payload-wide redaction guarantees (including which three fields are exempt and byte-exact), and the delivery semantics a receiver must design against — no retry, no ordering, no delivery receipt, be idempotent. It also states the stability contract explicitly: what a receiver may pin on versus what may grow. ClosesF-PR54-M10. Two claims were corrected against the code while writing it rather than copied forward.docs/standards/logging-observability.mdpromised webhook delivery is retried "up to 3 times with exponential backoff" — the shipped notifier has never retried, makes exactly one POST per event, and logs and abandons on failure; a receiver written to trust that promise would treat a missing event as impossible. The same rule list claimed atimeout=30maximum, which no code enforces (the schema'sge=1is the only bound, the default is 10s, and the notifier clamps up to a 1s floor).docs/reference/audit_event_catalog.md(+ TR) described masking asreason-scoped; it is now payload-wide, so that guarantee was widened to name every field it actually covers. The append-over-rename convention this page states was verified against history rather than assumed: no webhook event name has changed in a released version. One rename did happen in development (training.awaiting_approval→approval.required), but both commits land inside the same phase and the first tag containing either isv0.5.5, so no published release carried the old name. The page says exactly that instead of claiming an unbroken record. -
The pipeline manifest hash is documented as unkeyed, and a false security claim was corrected.
docs/reference/verify_annex_iv_subcommand.md(+ TR mirror) now carries an explicit threat model formetadata.manifest_hash: it is a plain SHA-256 produced bycompute_annex_iv_manifest_hash, a public function taking no secret, so it detects accidental corruption, careless edits and drift, and does not detect anyone who can write the manifest file — they edit a covered field, re-run the public function and write the digest back.hash_state: verifiedattests internal consistency, not authenticity. The page contrasts this with the audit log, which is keyed (FORGELM_AUDIT_SECRET, a per-runsha256(secret + run_id)key, per-line_hmactags andverify-audit --require-hmac) and notes thatverify-annex-ivhas no equivalent, so the manifest's integrity rests on the storage holding it. The claim being corrected — thatforgelm_versionand theannex_ivblock "cannot be edited to unlock the softer path without a hash mismatch" — was made in this same[Unreleased]section and indocs/roadmap/risks-and-decisions.md; it does not follow from an unkeyed digest. The append-only record carries a dated correction rather than an edit. Also documented: only stages whosestatusis exactlycompletedare deep-parsed, the three rules that keep a downgraded stage visible anyway, and the residual gap those rules do not close (a stage that completed with nogate_decisioncan still be downgraded to a recognised status without producing a violation) — hence the guidance to assertstages_examinedagainst the stage count the config declares, not against> 0and not againststages_total, since both come from the manifest itself. -
Superseded missing-evidence semantics corrected in
docs/reference/verify_annex_iv_subcommand.md(+ TR mirror). Both mirrors still described a missing per-stage artefact as unconditionally UNVERIFIED / exit1— the reader-side-only behaviour that was reverted for making deleted evidence route softer than corrupted evidence. They now document the shipped routing: VIOLATION / exit6when the run configured acompliance:block (the artefact was written and is gone), UNVERIFIED / exit1only when it did not (nothing was ever produced). The legacytraining_manifest.jsonfallback is documented as version-gated to pre-0.9.1manifests, including the note that only leading numeric release components are compared, so0.9.1rc1counts as0.9.1. -
Pipeline-mode verification documented in
docs/reference/verify_annex_iv_subcommand.md(+ TR mirror).--pipelinewas an undocumented flag on that page — absent from the synopsis, the flag table, and every example. It now carries a full section covering the chain manifest hash (what it covers, what it deliberately does not, and the three-wayhash_state), the per-stage evidence deep parse (what acompletedstage's evidence is now guaranteed to have survived, and the fail-closed table of every rejected on-disk state), the integrity-first precedence rule, and the JSON envelope'sstages_examined/evidence_verified/evidence_unverifiedcounters. DocumentsF-PR54-H7. The chain manifest hash it describes is not new in this release — it shipped inv0.8.0(commite7c3321, 2026-06-14) asF-P4-OPUS-20, before anyone noticed it also satisfied the open Phase 14.5 rowF-PR54-H6, which was simply never closed. This release documents that behaviour for the first time; it did not implement it. The distinction the section is built around: valid and verified are different states and the docs must not blur them. A manifest predating the hash stamp exits0withhash_state: "absent"and printsOK (UNVERIFIED); a hash-verified one exits0withhash_state: "verified"and printsOK. Both are exit0, and only one had anything compared. The page tells a CI author to asserthash_state == "verified",evidence_verified == stages_examinedandstages_examined > 0rather than trusting the exit code alone. -
The
_send(**extra)allowlist is documented as a non-change for receivers.docs/reference/webhook_schema.mdstates plainly that the allowlist contains exactly the keys the shippednotify_*methods already pass, so no field that used to arrive stops arriving and no payload changes shape. The hardening itself is recorded under### Security. DocumentsF-PR54-M11. -
Stale
forgelm/safety.pypointers swept from every prose surface. The sub-package split left the old path cited across the doc, site, notebook and agent-skill surfaces no guard was watching. Each reference was judged individually rather than rewritten in bulk: a pointer (prose telling a reader where something lives today) was retargeted at the specific submodule that now owns it —_gates.py,_orchestrator.py,_score_generation.pyand siblings, not vaguely at the package — while a record of the past (the CHANGELOG split announcement, the datedrisks-and-decisions.mdgrandfather-then-split pair, the_DEFERRED_SPLITSremoval note) was left exactly as written, because editing it would rewrite history. 17 files changed acrossdocs/guides/,docs/reference/,docs/standards/,docs/usermanuals/{en,tr}/,notebooks/,site/and both skill trees, with the EN and TR mirrors kept structurally aligned. -
The canonical Configuration Reference user manual (EN + TR) no longer documents a fabricated schema. The
training,data,synthetic,compliance,evaluation/llm_judge,model,lora, anddistributedblocks now match the realForgeConfigfield names exactly (copy-pasting an example previously failedforgelm --dry-run); fictional${...}interpolation syntax was removed. Several sibling user-manual pages (concepts/data-formats,reference/cli,evaluation/safety,deployment/gguf-export,operations/air-gap,getting-started/project-layout) were corrected against the source of truth as well. -
Compliance-mapping docs no longer cite a nonexistent audit event as evidence. The ISO 27001 / SOC 2 control mappings and the deployer guide cited a fabricated
pipeline.training_startedevent; they now reference real emitted evidence (config_hashin the training manifest /human_approval.*chain, andmodel_integrity.jsonSHA-256 artifact hashes). A design doc's claim that ForgeLM HMAC-signs webhook payloads (it does not), a phantomwebhook.secret_envfield, and a Statement-of-Applicability tally that did not reconcile were also corrected. -
User-manual schema drift swept. The
model,lora, anddistributedYAML blocks acrossreference/configuration,reference/yaml-templates,training/lora, andtraining/distributed(EN + TR) now match the realForgeConfigfields; the SSRF blocklist docs list RFC 6598 CGNAT; thereference/configurationandreference/safety_eval_subcommandpages document the real audit-event payloads and the safety-classifier requirement. -
All remaining fabricated user-manual config keys corrected. The new schema-drift guard's 38-instance backlog (worst:
deployment/model-merging'smerge.algorithm/base_model/parameters/output→ realMergeConfigmethod/models/output_dir/…; plusevaluation.trend,evaluation.max_length,synthetic.teacher.*,training.optimizer,model.name, …) is cleared across 8 EN/TR page-pairs, and the guard is now enforced with--strictin CI and the local gauntlet. -
Top-level docs corrected. README's documentation table no longer hides fully-translated Turkish guides;
CLAUDE.md/AGENTS.mdno longer carry clickable links into a gitignored internal-only working-memory tree and now show thewizard/sub-package;CONTRIBUTING.md's self-review gauntlet matches the canonical one;site/READMEreflects the real site structure. The standards rulebook's CI-guard claims were reconciled against the actual.github/workflows/+tools/, and the example notebooks were reconciled against the current output schemas (notablysafety_evaluation.ipynb's results cell).
The following corrections came out of an execution-verified audit of all 134 user-manual pages and the marketing site against the code. They change no behaviour, but several of them change what an operator should do — the auto-revert one in particular.
- Auto-revert was documented as a rollback. It is a deletion. Every surface
that described it —
evaluation/auto-revert.md,training/dpo.md, and the marketing site in all six locales — said the run "rolls back to the last-good checkpoint"._revert_modelcallsshutil.rmtreeon the final model directory; there is no checkpoint restore anywhere in the revert path, and after a revert there is no fine-tuned model on disk at all. If you sized your recovery plan around ForgeLM leaving a serviceable earlier artefact behind, it does not — keep your own backups. The pages now also state the two behaviours that most often surprise: with the shipped defaultevaluation.auto_revert: falsenothing is deleted, the regressed model is promoted and the run exits0; and a safety failure withevaluation_completed: falseis never reverted regardless ofauto_revert. Themodel.revertedaudit-event payload is corrected to its real two fields (reason,detail). data/audit.mdanddata/secrets.mdrewritten against the now-live secrets gate — exit3,--allow-secrets, and an explicit statement that secrets are the only finding class that gates. The CI gate these pages previously recommended did not work: it readcross_split_overlapandpii_summary.severity, neither of which exists in the stdout envelope, so it failed on clean input. Replaced with a snippet verified to pass on a clean corpus and fail on a dirty one, plus a table of where the on-disk report and the stdout envelope diverge.- Four "Programmatic API" sections were not runnable.
data/quality-filter.mdcalled ascore_qualitythat has never existed;operations/vram-fit-check.mdcalledestimate_peak_memory/available_memory, likewise; anddata/pii-masking.mdplusguides/library_api.mdpassedlocale=/language=kwargs that raiseTypeError. All four rewritten from executed code. reference/cli.mdandreference/json-output.mdreconciled with the real envelopes. Theforgelm auditenvelope is corrected to its actual shape (flat{kind: count}PII/secrets maps,pii_severity.worst_tier,cross_split_leakage_pairs) with five phantom keys removed; theverify-annex-iv --pipelineenvelope (12 keys) and its four-way exit mapping are documented for the first time, as are--max-safety-regressionand its envelope key;forgelm safety-evalandforgelm verify-annex-ivgained CLI sections; the four pipeline flags (--stage,--resume-from,--force-resume,--input-model) were added to the top-level flag table; the phantommanifest_hash_presentkey was removed andpath/checks.metadata_erroradded, with hash fields corrected to bare-hex strings; and theapprovals/approve/rejectexamples now carry the required--output-dirand document exit2.- The compliance manual now discloses what the integrity artefacts actually
prove.
metadata.manifest_hashandmodel_integrity.jsonare unkeyed SHA-256 digests that any writer can re-stamp: they detect corruption, not adversarial tampering. That is now stated on all four surfaces that describe them, contrasted against the HMAC-protected audit log, which is the one artefact here with keyed integrity.verify-annex-iv --pipelineand the three-valuedaudit_corroboration.outcomeare documented, including thatunattested— the case whereFORGELM_AUDIT_SECRETwas never set — is never a pass. - Fabricated compliance content removed. The on-disk artefact tree is
corrected to the real six-file
compliance/bundle; the invented eight-section Annex IV list is replaced with the real nine§1–§9keys; the declaration-of-conformity scaffold claims are deleted (no such scaffold is emitted); the model-card page is rewritten against the real template (the documented Intended use / Out-of-scope use / Training data / Limitations / Citation / License sections are not emitted); the phantomcompliance.license,compliance.notesandcompliance.risk_assessment.*fields and thecli.legacy_flag_invokedandmodel_card_amendedevents are removed; the high-risk tier is documented as requiringevaluation.safety.enabled: true; and theverify-auditchain-break sample andverify-integrityexit-1 enumeration are corrected. operations/cicd.mddocuments the full0–6exit-code contract, adding the5(wizard cancelled) and6(integrity failure) rows it was missing.evaluation/benchmarks.mdnow states that revert-and-exit-3 requiresevaluation.auto_revert: true;evaluation/trend-tracking.mddocuments all sevensafety_trend.jsonlfields and how to read a risingunscored_countas classifier degradation rather than model regression.- Wrong constants and shapes corrected across the manuals: quality-filter
alpha
0.70(not0.55) and word-length window3.0–12.0(not3–10);training.learning_ratedefault2e-5(not2e-4); the safetylabelshape"unsafe S5"(not"unsafe\nS5"); the bundled probe set as 51 probes over 18 independent category slugs (not an extension of the S1–S14 Llama-Guard taxonomy); andforgelm doctor's probe list. - Fabricated features removed from the manuals: preference/KTO
format-specific audit checks, per-category PII redaction tags, secrets
false-positive guards (entropy / context-window / exclusion list), an
--output-jsonlflag, awebhook_failedaudit event, and an IPv4/IPv6 PII category — none of which exist. getting-started/first-run.md's quickstart flow now works end to end (it uses--outputso the config path it then references is real), andgetting-started/installation.mddocumentspython -m forgelmas an equivalent entry point.- Marketing site corrected against the code (
site/, all six locales): the auto-revert rollback claim above; a quickstart page whose copy-paste flow did not work;compliance.html's artefact tree no longer listsdata_audit_report.jsoninsidecompliance/; the "automatic" Annex IV + Llama Guard hero claim is now qualified by its real preconditions; the probe count corrected50→51; HMAC activation no longer described as requiring KMS; and the unqualified "confidence-weighted scoring" headline corrected to binary-by-default.
0.9.0 — 2026-07-05
- transformers 5.x migration (breaking). Raised the core dependency floor to
transformers>=5.3.0,<6.0.0and cascaded the co-dependencies it requires:torch>=2.4.0,huggingface_hub>=1.3.0,<2.0.0,peft>=0.19.0,accelerate>=1.4.0,datasets>=4.7.0,<6.0.0,trl>=1.0.0, andrequests>=2.32.2(trl 1.0 pulls theaccelerate>=1.4/datasets>=4.7floor; datasets 4.7 pullsrequests>=2.32.2). transformers 5.3.0 is the first release carrying the fix for CVE-2026-4372; the previous<5.0.0pin could not reach it. Thetest-min-depsnightly floor andtools/pip_audit_ignores.yamlwere updated to match the new minimums. from_pretraineddtype kwarg. Renamedtorch_dtype=→dtype=at the two base-model load sites (export.py,synthetic.py);torch_dtypeis a deprecated alias in transformers 5 slated for removal. Behaviour is unchanged.
- transformers 4.x support (breaking). ForgeLM now requires transformers 5.x.
The
safe_serialization=Truekwarg (removed fromsave_pretrainedin transformers 5, where safetensors is the enforced default) was dropped from the three model-save call sites (export.py,merging.py,trainer.py) — the on-disk output is unchanged. - Intel Mac (x86_64) support (breaking). transformers 5 requires
torch>=2.4, for which PyPI publishes no x86_64-Darwin wheel, so that platform can no longer install ForgeLM's core stack. Apple Silicon, Linux, and Windows are unaffected. The now-mootnumpy<2; darwin x86_64ABI-guard marker was removed with it.
- CVE-2026-4372 — a critical
AutoModelForCausalLM.from_pretrained()RCE in transformers <5.3.0 (a maliciousconfig.json_attn_implementation_internalfield downloads and executes attacker code, bypassingtrust_remote_code) — is resolved by thetransformers>=5.3.0floor above. Both transformers suppressions intools/pip_audit_ignores.yaml—CVE-2026-1839(fixed in 5.0.0rc3) andPYSEC-2025-217/CVE-2025-14929(X-CLIP RCE, OSV last-affected 5.0.0rc0) — are now inert under the raised pin and were removed.
0.8.0 — 2026-06-16
- Model-integrity verification. New
forgelm verify-integrity MODEL_DIRsubcommand andforgelm.verify_integrity()/VerifyIntegrityResultpublic API: re-hashes a trained model directory against its EU AI Act Article 15model_integrity.jsonSHA-256 manifest (written by the compliance export at training time) and reportschanged/removed/addedartifacts. Exit0(all match) /1(mismatch or input error) /2(runtime I/O failure);--output-format jsonfor CI gates. Bumps__api_version__to1.1.0. - Config-driven merge hyperparameters.
merge.ties_trim_fraction,merge.dare_drop_rate, andmerge.dare_seedexpose the TIES/DARE knobs that were previously fixed module constants (defaults unchanged:0.2,0.3,42). Operators can now reach paper-faithful sparsity from YAML. - Config-driven synthetic sanity bound.
synthetic.sanity_failure_rate(default0.2) replaces the hardcoded warn-only failure-rate threshold inforgelm --generate-data; it is independent ofmin_success_rate, which still gates the exit code.
- Config validation hardened.
distributed.strategyis now aLiteral["deepspeed", "fsdp"](an unsupported value such ashorovodused to validate and then silently run single-GPU).data.mix_rationow rejects non-finite weights (NaN / inf) and must carry exactly one weight per dataset (1 primary + len(extra_datasets)); a length mismatch raised no config error and silently fell back to uniform mixing at runtime. Both now fail fast at config time (exit 1).
training.sample_packingis now a deprecated alias fortraining.packing. It was previously a documented-but-unconsumed field (a silent no-op); it now forwards topackingwith aDeprecationWarningso the documented behaviour actually fires. Usepackinginstead —sample_packingis removed in v0.9.0. See docs/standards/release.md.
evaluation.staging_ttl_days(deprecated in v0.7.0) is removed. Use the canonicalretention.staging_ttl_days;EvaluationConfigisextra="forbid", so the legacy key now raises a validation error instead of forwarding.Errata (2026-07-19): "deprecated in v0.7.0" above is wrong and is left in place because released entries are not rewritten. The field was actually deprecated in v0.5.5, whose entry reads "Removal scheduled for v0.7.0" — the scheduled-removal version was misread as the deprecation version. Counting only true MINOR (Y-digit) releases per
docs/standards/release.md's Versioning table — a patch tag such as v0.5.5 anchors to its v0.5 minor line and adds no separate hop — the real warning window was three minors (v0.5.5 → v0.6.0 → v0.7.0 → v0.8.0), not one. The same misreading applies to the--data-audit PATHalias in the bullet below: it was deprecated in v0.5.0 ("Removal targeted no earlier than v0.7.0"), also a three-minor window (v0.5.0 → v0.6.0 → v0.7.0 → v0.8.0) under the same count, since both fields' deprecations anchor within the same v0.5 minor line. Neither correction changes what v0.8.0 shipped — only the history it cites.forgelm --data-audit PATHCLI flag (deprecated in v0.7.0) is removed. Use the first-classforgelm audit PATHsubcommand — identical behaviour and output.argparsenow rejects the flag (exit 2).cli.legacy_flag_invokedaudit event is no longer emitted (it recorded use of the removed--data-auditflag) and has been dropped from the audit-event catalog.
- Eval artefact privacy-redaction documented.
safety_results.jsonandjudge_results.jsonhave been privacy-redacted by default since v0.7.0; this entry adds the previously missing CHANGELOG documentation. Rawprompt/response/ judgereasonstrings are not persisted unless the opt-in flagsevaluation.safety.include_eval_samplesandevaluation.llm_judge.include_eval_samples(both defaultfalse) are set. This honours GDPR / EU AI Act Article 10 privacy-by-default — adversarial prompts and judge reasoning can quote sensitive content. Set the flag totrueonly for debugging. - Nightly pip-audit gate — transformers PYSEC-2025-217 / CVE-2025-14929.
Advisory records X-CLIP checkpoint-conversion deserialization RCE
(CVSS AV:L/UI:R — local + user-interaction required). No fixed version in
the
transformers<5.0.0range. Codebase check 2026-05-24: no X-CLIP usage inforgelm/, no directtorch.loadcalls. Risk accepted intools/pip_audit_ignores.yaml; re-evaluate each release cycle. - Pipeline configs with
pipeline:+retention.staging_ttl_days+ anyevaluation:block no longer raise a falseConfigError ("Conflicting staging_ttl_days values"). The stage-merge round-trip re-materialised the deprecatedstaging_ttl_days=7default as if the operator had written it; the dump now excludes unset defaults.
0.7.0 — 2026-05-14
Phase 14 (Multi-Stage Pipeline Chains) closes the "operators have to write
shell wrappers to chain SFT → DPO → GRPO" gap that's lived in the issue
queue since v0.4. One YAML, one CLI invocation, one Annex IV manifest for
the whole chain — including auto-chained inputs, per-stage gates,
crash-safe resume, and 7 new pipeline-scoped audit events that join on a
single top-level run_id. The release also lands a critical SSRF
hardening for outbound webhook / judge / synthetic destinations
(issue #14).
Single-stage configs reach forgelm/trainer.py byte-identical to
v0.6.0; the orchestrator module is never imported when no pipeline:
block is present.
pipeline:config block at the root ofForgeConfigchains one or more training stages (typically SFT → DPO → GRPO) into one config-driven run. New Pydantic modelsPipelineStageandPipelineConfigenforce stage-name uniqueness,^[a-z0-9_]{1,32}$identifier shape, at least 1 stage (min_length=1— single-stage ergonomics are still better served by omitting thepipeline:block, but the schema accepts a 1-element pipeline so an operator can iterate up to a multi-stage chain without re-shaping the YAML), and explicit-trainer_typeaudit-clarity validation per stage. Section-wholesale inheritance:model/lora/training/data/evaluationblocks inherit from the root if omitted or fully replace it when set.distributed/webhook/compliance/risk_assessment/monitoring/retention/synthetic/merge/authare root-only and rejected per-stage withEXIT_CONFIG_ERROR (1).forgelm/cli/_pipeline.pyorchestrator drives the chain end-to-end. Auto-chains each stage'smodel.name_or_pathto the previous stage's output, persists state atomically to<pipeline.output_dir>/pipeline_state.jsonafter every transition, and emits 7 new audit events:pipeline.started,pipeline.stage_started,pipeline.stage_completed,pipeline.stage_gated(when a stage exitsEXIT_AWAITING_APPROVAL— review-cycle F-N-1),pipeline.stage_reverted,pipeline.force_resume(operator- approved stale-config override — review-cycle F-B-2), andpipeline.completed. Every entry's top-levelrun_idis pinned to the pipeline run id (review-cycle final-round F-B-1). Existingtraining.*per-stage events fromForgeTrainerare preserved — pre-existing Slack / Teams dashboards filtering ontraining.failurekeep working unchanged.- CLI flags
--stage <name>(single-stage filter for audit / re-run),--resume-from <name>(stage-boundary resume),--force-resume(stale-config-hash override), and--input-model <path>(auto-chain escape hatch, recorded withinput_source: cli_overridein the audit log). --dry-runmulti-stage validation — when the config carries apipeline:block, dry-run validates every stage's merged config + the chain-integrity assertion (stage N's auto-chained input is under stage N-1'soutput_dir). Errors are collected before exiting (à lapytest --collectonly).- Pipeline Annex IV manifest —
compliance/pipeline_manifest.jsonat<pipeline.output_dir>indexes per-stagetraining_manifest.jsonfiles into one verifiable chain artefact, carryingpipeline_run_id+pipeline_config_hash(SHA-256 of the YAML bytes) for reproducibility.forgelm verify-annex-iv --pipeline <run_dir>walks the index, checks chain integrity, and asserts every referenced per-stage manifest exists on disk. - Webhook notifications —
WebhookNotifier.notify_pipeline_started,notify_pipeline_completed, andnotify_pipeline_revertedmirror the orchestrator's audit events; gated by the existingnotify_on_start/notify_on_success/notify_on_failureconfig flags so operators silencing per-stage events also silence the pipeline-level pings. - Bilingual operator guide — new
docs/guides/pipeline.md+docs/guides/pipeline-tr.md(canonical "first pipeline" walkthrough, inheritance matrix, CLI semantics, Annex IV verifier flow, Limitations section).config_template.yamlgains a commented-outpipeline:example block.
- Backward compatibility preserved. A config file without a
pipeline:key reachesforgelm/trainer.pybyte-identically to v0.6.0; the orchestrator module is never imported. compliance.pyexports four new symbols:generate_pipeline_manifest(called by the orchestrator after every transition),export_pipeline_manifest(atomic write to<pipeline.output_dir>/compliance/pipeline_manifest.json),verify_pipeline_manifest(manifest: dict) -> List[str](in-memory structural + chain-integrity check, returns a list of human-readable violations — empty when valid), andverify_pipeline_manifest_at_path (pipeline_dir: str) -> List[str](disk-backed wrapper used by theforgelm verify-annex-iv --pipeline <dir>CLI mode). The shared validator lives in private helper_verify_manifest_payloadso the in-memory and disk-bound entry points cannot drift.
Addresses the consolidated reviewer-pass findings from Phase 14 review-response (PR #53) — 3 blocking + 4 significant + Gemini's 3 inline comments — against the initial Phase 14 merge candidate:
- F-B-1 —
forgelm --config pipeline.yaml --dry-runnow reaches the pipeline orchestrator's per-stage validator instead of the legacy single-stage dry-run path; the_dispatch.pyordering was inverted (no-train-mode ran before the pipeline branch). - F-B-2 —
--force-resumenow emits apipeline.force_resumeaudit event carryingold_config_hash+new_config_hashso compliance reviewers can distinguish an operator-approved override from a normal resume (was previously a WARNING log only — invisible in the append-only JSONL stream). - F-B-3 — pipeline manifest verifier now compares every chain
stage against its immediate predecessor, not the most-recent
stage with an
output_model. Pre-fix, a manifest where stage N-1 failed without saving anoutput_modelcould appear to chain stage N from stage N-2, masking the broken link. - F-F-1 / F-G-3 (Gemini) — auto-chain existence guard now
checks the resolved model directory itself, not its parent — used
to accept
./stage1/even when./stage1/final_modelwas missing. - F-F-2 —
--input-model ""(empty string) is normalised toNoneat dispatch time so it no longer slips past the truthy guard and silently overwrites the auto-chained model path with an empty string. - F-F-3 —
_validate_resume_statenow returns its refusal through the normalrun()flow instead ofsys.exit-ing mid-method, so every refusal path emits the same audit-log finalisation and the test surface uses one uniformassert code == EXIT_CONFIG_ERRORshape. - F-S-2 / F-N-2 — verifier surfaces stages still in
runningstatus on a finalised manifest as achain_integrity_violationcandidate (a tell of a crashed orchestrator). Also_load_state_filecatchesKeyError/TypeError/ValueError/AttributeErroraround_deserialise_stateso a tampered state file produces an actionable config error rather than a Python traceback. - F-N-1 — gated stages now emit a dedicated
pipeline.stage_gatedaudit event (waspipeline.stage_completedwith agate_decision=approval_pendingsub-field). Lets dashboard / SIEM filters distinguish the gate flow on the event name alone. - F-G-1 (Gemini) — pipeline dry-run now flags
training.output_dircollisions across stages (two stages writing to the same dir would silently overwrite each other's checkpoints + per-stage Annex IV manifests). Covers both the explicit-override and inherited-from- root collision cases.
- Webhook / judge / synthetic SSRF guard — DNS-rebinding TOCTOU
hardening (issue #14). Pre-fix,
_is_private_destination()pre-resolved the hostname andrequests.post()then ran its own DNS lookup at connect time; an attacker-controlled DNS server with TTL=0 could return a public IP on the first lookup (passing the guard) and a private IP on the second (whenrequestsconnected), leaking the payload + bearer token to a private destination (loopback, RFC1918, or cloud IMDS at169.254.169.254). New_resolve_safe_destination()helper resolves the hostname exactly once andsafe_post/safe_getrebuild the outbound URL with the returned public IP literal sorequestsnever re-resolves. The original hostname is preserved via theHostheader (and SNI for HTTPS) usingrequests_toolbelt.adapters.host_header_ssl.HostHeaderSSLAdapterso virtual-hosted endpoints (Slack / Teams / Discord) and certificate validation still work correctly.allow_private=Truecallers (operator-blessed internal destinations) keep the legacy flow so split-horizon DNS / in-cluster resolution still works.requests-toolbelt>=1.0.0,<2.0.0is now a hard dependency.
0.6.0 — 2026-05-11
Phase 15 (Ingestion Pipeline Reliability) Wave 1 + Wave 2. Closes the silent-failure gap the 2026-05-11 pilot exposed across PDF / DOCX / EPUB / TXT / MD ingestion plus the user-facing playground notebook. Five review-absorption rounds (Gemini / CodeRabbit / Sonar / Codacy + independent self-review) ship in the same release.
forgelm/_pypdf_normalise.py— Turkish glyph normalisation profile (turkishdefault,noneopt-out, future-pluggable--normalise-profile) mapping the audit-measured pypdf font-fallback artefacts (ø Õ ú ÷ →İ ı ş ğ •) back to their correct Turkish characters at chunk-write time.forgelm/_script_sanity.py— language-aware Unicode-block sanity check (tr/en/de/fr/es/it/pt) that fires a WARNING + structuredscript_sanity_summaryblock when the out-of- script ratio exceeds the calibrated 1.5 % threshold. Catches both pypdf font corruption and TXT encoding mis-routing.forgelm ingest --language-hint LANG+--script-sanity-threshold--normalise-profile {turkish,none}+--no-normalise-unicodeCLI flags wiring Tasks 2 + 3 to the operator surface.
forgelm ingestend-of-run quality pre-signal (Task 4) — three cheap row-level checks (alpha-ratio, weird-char ratio, repeated-line ratio) emitting[WARN] N/M chunks below ingestion quality threshold- a structured
quality_presignalblock innotes_structured. Opt out via--no-quality-presignal.
- a structured
forgelm doctorpypdf-normalise diagnostic (pypdf_normalise.turkish) — confirms the glyph-normalisation table loaded and round-trips cleanly without running a test ingest.- DOCX explicit header / footer extraction (Task 6) —
_extract_docxreadsdoc.sections[i].header.paragraphs+.footer.paragraphsand subtracts those lines from the body before chunking. - EPUB spine-order + nav / cover / copyright skip (Task 7) —
_extract_epubiteratesbook.spine(reading order) instead of file order; default skip-list (nav,cover,copyright,colophon,titlepage,frontmatter) opt-out via--epub-no-skip-frontmatter. - TXT UTF-8 BOM strip + MD YAML frontmatter detection (Task 8) —
_extract_text/ new_extract_markdownstrip the BOM viaencoding="utf-8-sig"and detect---\n...\n---\nYAML frontmatter;--keep-md-frontmatteropts back in. forgelm/ingestion.strip_paragraph_packed_headers— second-pass dedup against paragraph-packed text catches header lines that survived the page-level pass.tests/test_ingestion_reliability.py— 36 regression tests locking the Wave 1 + Wave 2 behaviour across PDF / DOCX / EPUB / TXT / MD fixtures.
forgelm ingest --strip-pattern REGEX(Task 11) — operator- controlled regex stripping with ReDoS guard. Patterns are structurally validated at CLI-parse time (rejects nested unbounded quantifiers like(a+)+band.*?+ back-reference under DOTALL per the SonarCloudpython:S5852shape rule), wrapped in a 5-second per-pattern SIGALRM budget on POSIX. Opt out of the timeout via--strip-pattern-no-timeout.forgelm ingest --page-range START-END(Task 12) — restrict PDF extraction to a contiguous page slice (1-indexed inclusive). Validation failures (start < 1,start > end,start > page_count) abort the run withEXIT_CONFIG_ERRORvia a newIngestParameterError(ValueError)that bypasses the per-file soft-fail catch.- PDF front-matter / back-matter heuristic (Task 13, default ON) —
three-signal heuristic (alpha < 0.45 + underscore > 0.10 + ≥ 5
inline page-number matches) drops up to 12 leading + 12 trailing
pages and emits a WARNING +
frontmatter_pages_droppedfield innotes_structured. Opt out via--keep-frontmatter. forgelm ingest --strip-urls {keep,mask,strip}(Task 14) — detected URLs are masked with[URL], stripped outright, or kept (default). Independent of--all-mask(URL handling is a content-shape decision, not a GDPR redaction).- PDF multi-column layout warning (Task 15) — samples the first
three pages' Tj-text positions via pypdf's
visitor_textcallback, fires a WARNING when a > 30 %-of-page-width two-cluster gap is detected. No fix attempt — operator switches strategy / pre- processes externally.
forgelm audit --quality-filteris now default-ON in v0.6.0 (Task 5). Pre-v0.6.0 invocations with explicit--quality-filterkeep identical behaviour. Operators wanting the pre-v0.6.0 opt-in semantics pass the new--no-quality-filtercompanion flag._strip_repeating_page_lineswindow-based dedup (Task 1) — replaces the pre-Phase-15 outermost-row-only iteration. Catches variable-outer-line + constant-deeper-line corpora (the audit §1.1 trap) by inspecting the top-3 / bottom-3 rows per page on every pass. A second pass runs after paragraph packing to mop up survivor headers.notebooks/ingestion_playground.ipynb(Task 9) — Cell 5 exposesCHUNK_TOKENS+TOKENIZER+LANGUAGE_HINTknobs; Cell 8 markdown explains the new quality-filter checks; Cell 9 passes--quality-filterexplicitly for forward-compat across v0.5 → v0.6; Cell 10 pretty-prints thequality_summaryblock alongside the existing PII / secrets / near-duplicate / language sections.IngestionResultschema — additive Phase 15 fields:pdf_paragraph_packed_lines_stripped,script_sanity_triggered,strip_pattern_substitutions,urls_handled,frontmatter_pages_dropped. No pre-Phase-15 key was renamed.
Five review-absorption rounds (Gemini + CodeRabbit + Sonar + Codacy + independent self-review) shipped in the same release. Highlights:
forgelm/cli/_training.py::_preflight_numpy_torch_abi— any unexpected exception from the underlying probe (corrupted torch install wheretorch.__version__raisesAttributeError, etc.) is now caught and converted into a structuredabi_preflight_crashedJSON envelope. Previously the raw Python traceback would pre-empt the--output-format jsoncontract that every other CLI failure path honours. Exit code staysEXIT_TRAINING_ERROR(= 2), matching the broken-ABI verdict so CI/CD branching doesn't need to distinguish "ABI bad" from "ABI probe died".forgelm ingest --language-hint— a hint outsideforgelm._script_sanity.SUPPORTED_LANGUAGES(e.g.zh) now triggers a WARNING at CLI dispatch time naming the supported codes instead of silently no-opping the script-sanity layer. Round-5 self-review C-B finding.- EPUB skip-list whole-token match —
recovery.xhtmlno longer matchescovervia substring;_epub_item_matches_skipsplits filenames + EPUB-3 manifest properties on path/extension/separator characters and matches whole tokens. Skipped items emit a WARNING naming the affected files. Round-1 C-1 + round-3 S-C findings. - Default
normalise_profileflipped from"turkish"to"none"; CLI dispatcher + libraryingest_pathboth auto-derive"turkish"only when--language-hint tris set. Prevents silent rewrites of legitimate non-Turkish letters (Norwegianø, EstonianÕ, math÷). Round-1 C-2 finding. - ReDoS validator caught escape-shape variants (
(\w+)+x,(\d+)+x,(\s+)+x,(\w+\s+)+x) the pre-round-1 backward-walk validator skipped, and now clamps the per-pattern SIGALRM tomin(timeout_s, previous_remaining)so nested calls cannot extend an outer caller's deadline. Round-1 S-1 + round-2 alarm clamp findings. - Operator-controlled-string injection vector in
IngestParameterError/ decrypt-fail /Could not open PDFmessages — paths now repr-escape via{path!r}so ANSI escape sequences / control chars / embedded quotes cannot leak into the rendered error. Round-4 C-A finding. - Quality-presignal false positive on clean small corpora —
chunks below 80 non-whitespace characters skip the alpha-ratio
check so a 5-paragraph 41-char TXT no longer emits
[WARN] 1/1 chunks below ingestion quality threshold. Round-4 C-B finding. - Front-matter heuristic dot-leader detection
(
r"[._]{3,}"), denominator parity withalpha_ratio(non- whitespace chars), and alpha threshold tightened 0.45 → 0.30. Catches dotted-leader ToCs the pre-round-3 underscore-only count missed; protects realistic form templates via the 3-signal AND filter. Round-3 + round-4 + round-5 findings.
forgelm/cli/_training.py::_preflight_numpy_torch_abi— any unexpected exception from the underlying probe (corrupted torch install wheretorch.__version__raisesAttributeError, etc.) is now caught and converted into a structuredabi_preflight_crashedJSON envelope. Previously the raw Python traceback would pre-empt the--output-format jsoncontract that every other CLI failure path honours. Exit code staysEXIT_TRAINING_ERROR(= 2), matching the broken-ABI verdict so CI/CD branching doesn't need to distinguish "ABI bad" from "ABI probe died".CLAUDE.md— exit-code contract was stated as0/1/2/3/4, but the canonical table indocs/standards/error-handling.mddocumented0/1/2/3/4/5since v0.5.5 (Phase 22 addedEXIT_WIZARD_CANCELLED = 5). CLAUDE.md now matches the actual contract — the standard was right, the agent guidance was stale.docs/roadmap/completed-phases.md::Phase 12summary — Tier 1 status line claimed a[ingestion-secrets]extra "viadetect-secretswith regex fallback", but that extra was never published; only[ingestion-pii-ml]exists inpyproject.toml's extras surface. Reworded to record the historical plan accurately ("detect-secretsintegration was originally planned as a[ingestion-secrets]extra but deferred — only[ingestion-pii-ml]ultimately shipped"). Pure docs accuracy fix.
0.5.7 — 2026-05-11
Patch release on top of v0.5.6. Two production blockers and one UX
gap that bit Intel Mac operators:
- SFT trainer
TypeErroron moderntrl(0.13 + 1.x). trl 0.13 renamedSFTConfig.max_seq_length→max_lengthand removed the old kwarg; v0.5.6 was still passingmax_seq_lengthunconditionally, soforgelm --config <yaml>crashed withTypeError: SFTConfig.__init__() got an unexpected keyword argument 'max_seq_length'on any environment that pulled a current trl wheel (notably the Colab defaultpip install forgelmpath). - NumPy 2.x ↔ torch 2.2 binary-ABI mismatch exposed by the v0.5.6
Intel Mac torch revert. With
torch>=2.2.0as the floor and no upper-pin on numpy, pip resolvednumpy>=2on Intel Mac (x86_64) hosts; torch 2.2 was compiled against NumPy 1.x and silently degraded its numpy bridge with_ARRAY_API not found. - Operator UX: the cryptic
NameError: name '_C' is not definedthat surfaced from the ABI mismatch would previously bite mid-training with no actionable hint. v0.5.7 ships a doctor probe and a training-pipeline preflight so the operator gets a single-linepip install 'numpy<2'remediation instead.
Release engineering: this patch absorbs five review rounds against
PRs #44 / #45 — the SFT + ABI fixes are listed under Fixed below;
the UX additions (preflight + doctor probe + JSON-envelope contract +
shared _abi_check helper) are under Added; pure test/CI
hardening (the sys.modules pollution cascade fix, the new CI guard,
pytest-randomly adoption) is under Internal.
forgelm doctor—numpy.torch_abiprobe. New diagnostic inspects torch + numpy major-version pairing and emits afailwith apip install 'numpy<2'remediation hint when a torch < 2.3 install is paired with NumPy ≥ 2 (the canonical Intel Mac install failure mode). Probe imports torch / numpy with warnings suppressed so the probe itself does not emit a duplicate PythonUserWarning(the underlying torch C++fprintf(stderr, "_ARRAY_API not found", …)message is outside Python's warnings machinery and cannot be intercepted from a long-lived CLI process — documented honestly in the probe docstring). Surfaces in theforgelm doctor --output-format jsonenvelope so CI consumers catch the issue programmatically.- Training-pipeline ABI preflight (
forgelm/cli/_training.py::_preflight_numpy_torch_abi). A shared helper atforgelm/cli/_abi_check.pyruns the same probe asforgelm doctor, and_run_training_pipelinenow calls it before importing the heavy stack. An operator who hits a residual Intel Mac NumPy 2 mismatch (env drift, out-of-bandpip install -U numpy) now sees a single-line error with the exact remediation command instead of a crypticNameError: name '_C' is not definedfrom torch mid-training. The PEP 508 marker inpyproject.tomlis the primary fix for fresh installs; the preflight is the second line of defense for drifted environments. Any unexpected crash from the probe itself converts into a structuredabi_preflight_crashedJSON envelope (sameEXIT_TRAINING_ERROR= 2 class) rather than a raw Python traceback. The two envelope shapes (numpy_torch_abi_mismatch+abi_preflight_crashed) are documented indocs/usermanuals/{en,tr}/reference/json-output.mdfor CI consumers.
forgelm/trainer.py::_get_training_args_for_type— the SFT branch now inspectsSFTConfig.__init__'s signature at runtime and picks the correct sequence-length-cap parameter name. trl 0.13+ (including the 1.x line) receivesmax_length; trl 0.12.x — still within thepyproject.tomlfloortrl>=0.12.0,<2.0.0— keeps receivingmax_seq_length. If a future trl release exposes neither name as a discoverable parameter, the branch now raisesValueError(not warn-and-continue) listing the actually-detected parameters — silently dropping an explicitmodel.max_lengthYAML setting would violatedocs/standards/error-handling.md"no silent failures". No code change for DPO / SimPO / KTO / ORPO / GRPO trainers (their*Configparameters were not affected by the rename).pyproject.toml— Intel Mac (x86_64) NumPy 2 ABI mismatch. Addednumpy<2; sys_platform == 'darwin' and platform_machine == 'x86_64'so pip's resolver caps numpy at the 1.x line on the only platform that is wheel-locked to torch 2.2.x. Other platforms keep numpy unconstrained — torch 2.3+ on Linux / Apple Silicon / Windows is binary-compatible with NumPy 2.x. Affectedpip install forgelmusers on Intel Mac since the v0.5.6 torch revert.
- Test-pollution cascade fix. Three pre-existing tests
(
tests/test_doctor.py× 2,tests/test_quickstart_compat.py× 1) poppedsys.modules['torch']/sys.modules['numpy']without restoring them; the stranded modules half-initialised every subsequentimport torchin the pytest session (torch._Cnever re-bound), causing every downstreamfrom trl import SFTConfigand a long tail of merging / moe / quickstart / wizard / judge / safety tests to fail in the full suite even though they passed in isolation (35 spurious failures observed on the Intel Mac dev box). Swapped formonkeypatch.delitemso the modules are auto-restored on test teardown. - New CI guard
tools/check_no_unguarded_sys_modules_pop.py— fails on anysys.modules.pop("torch"|"numpy"|"trl"|…)ordel sys.modules["…"]withoutmonkeypatch.delitem. Wired into the self-review gauntlet (CLAUDE.md) and.github/workflows/ci.ymlso the regression class becomes impossible to silently re-introduce. pytest-randomly>=3.15.0,<4.0.0added to[dev]extra. Shuffles test order per session so order-dependent bugs (the failure mode above) surface in CI instead of waiting for a Python micro-bump or runner change to flip collection. Reproduce a specific shuffle withpytest --randomly-seed=<n>.forgelm/cli/_abi_check.py— new shared helper module carries the ABI verdict + remediation logic. Both the doctor probe and the training preflight consume it as a single source of truth. Includes a(0, 0)parse-fallback short-circuit so corporate forks with non-semver torch / numpy tags can't trip a false-positiveABI_BROKEN.forgelm/cli/subcommands/_doctor.py::_check_numpy_torch_abirefactored to call into the shared helper rather than duplicating the version-parsing + threshold logic. Exhaustive-enum guard at the end nowraisesRuntimeErrorinstead ofassert-ing, sopython -Odoesn't strip the check.- Documentation hygiene.
CLAUDE.mdexit-code contract line now reads0/1/2/3/4/5to match the canonical table indocs/standards/error-handling.md(the standard had documentedEXIT_WIZARD_CANCELLED = 5since v0.5.5; the agent guidance was stale).docs/roadmap/completed-phases.mdPhase 12 Tier 1 status line no longer advertises a[ingestion-secrets]extra that was never published — only[ingestion-pii-ml]exists. One leftover(Faz 12)Turkish residue in the v0.5.2 CHANGELOG entry rewritten to(Phase 12).
0.5.6 — 2026-05-10
Status: Released to PyPI 2026-05-10 via the cross-OS publish
workflow (.github/workflows/publish.yml)
which gates PyPI publish on 12 wheel-install matrix combos
(3 OS × 4 Python). GitHub Release:
v0.5.6.
Patch release. Reverts the v0.5.5 torch>=2.3.0 minimum back to
torch>=2.2.0 to restore Intel Mac (x86_64) installability. The
torch>=2.3 floor in v0.5.5 was inaccurate — no v2.3-specific
PyTorch API is referenced in production code. The single citation
in tests/test_grpo_reward.py is a comment explaining the test's
graceful skip when trl.GRPOTrainer's lazy import fails on a
torch/trl mismatch; the skip pattern
(pytest.mark.skipif(not grpo_patchable, ...)) already handles that
case across torch 2.2 + 2.3. Production FSDP usage in
forgelm/trainer.py delegates string options to
transformers.TrainingArguments and never imports
torch.distributed.fsdp.FSDPModule directly.
pyproject.toml—torch>=2.3.0,<3.0.0→torch>=2.2.0,<3.0.0. Other dependencies untouched.
pip install forgelmsilently downgrading to v0.5.0 on Intel Mac (x86_64) hosts. The PyTorch Foundation stopped publishingtorch>=2.3wheels for Intel Mac (only Apple Silicon / Linux / Windows wheels exist for 2.3+). v0.5.5'storch>=2.3.0requirement caused pip's resolver to silently fall back to v0.5.0 (which pinnedtorch>=2.1.0) for those hosts —pip install forgelmappeared to succeed but installed a year-old version with no GDPR / Library API / operational subcommands. v0.5.6 lowers the floor totorch>=2.2.0, the highest minor available across all supported platforms (including Intel Mac × Python 3.11). Existing v0.5.0 users on Intel Mac can now upgrade withpip install -U forgelmand reach v0.5.6 cleanly.
0.5.5 — 2026-05-10
"Closure Cycle Bundle + Phase 22 Wizard + Site Documentation Sweep." v0.5.5 consolidates the closure-cycle backlog (Library API, GDPR right-of-erasure + right-of-access, ISO 27001 / SOC 2 alignment, operational subcommands, supply-chain security, cross-OS release matrix) and three follow-up PRs that landed before the PyPI tag was cut (Phase 22 CLI wizard modernisation, site documentation correction sweep, release-prep + nightly pip-audit gate fix).
- Stable Python entry points for every CLI subcommand.
forgelm/__init__.pyis now a strict lazy-import facade with PEP 562__getattr__,__dir__enumeration of the public surface, aTYPE_CHECKINGblock formypy --strictconsumers, and per-symbol caching inglobals().import forgelmdoes not pulltorch; resolved values are zero-cost on the second access. Newforgelm/_version.pyseparates__version__(package) from__api_version__(Python library API contract);forgelm/py.typedmarker shipped viapyproject.tomlpackage-data. The__all__enumerates configuration (load_config,ForgeConfig,ConfigError), training (ForgeTrainer,TrainResult), data (prepare_dataset,get_model_and_tokenizer,audit_dataset,AuditReport), PII / secrets / dedup utilities (detect_pii,mask_pii,detect_secrets,mask_secrets,compute_simhash), compliance (AuditLogger,verify_audit_log,VerifyResult), verification toolbelt (verify_annex_iv_artifact,VerifyAnnexIVResult,verify_gguf,VerifyGgufResult), webhooks (WebhookNotifier), and auxiliary (setup_authentication,manage_checkpoints,run_benchmark,BenchmarkResult,SyntheticDataGenerator).
forgelm purge— three-mode dispatcher:--row-id <id> --corpus <path>— atomic JSONL row erasure with a SHA-256(salt + id) hashed audit event. A per-output-dir salt at<output_dir>/.forgelm_audit_salt(mode0600) persists regardless of theFORGELM_AUDIT_SECRETtoggle; env-var-set invocations XOR the persistent salt with the secret prefix and recordsalt_source="env_var"so a salt-source change between invocations is detectable in the chain.--run-id <id> --kind {staging,artefacts}— run-scoped artefact erasure (staging directory or compliance bundle).--check-policy— read-only retention-policy violation report (always exits 0; report-not-gate by design).
RetentionConfig— new Pydantic block with four horizons (audit_log_retention_days=1825,staging_ttl_days=7,ephemeral_artefact_retention_days=90,raw_documents_retention_days=90) andenforce ∈ {log_only, warn_on_excess, block_on_excess}.EvaluationConfig.staging_ttl_daysalias-forwards toretention.staging_ttl_dayswith a singleDeprecationWarning; conflicting values raiseConfigError. Removal scheduled for v0.7.0.- Six new
data.erasure_*audit events catalogued bilingually (three core erasure events + three operator-warning events), plus an operator guide atdocs/guides/gdpr_erasure.md(+ TR mirror).
forgelm reverse-pii --query VALUE [--type literal|email|phone|tr_id|us_ssn|iban|credit_card|custom] [--salt-source per_dir|env_var] JSONL_GLOB...— walks JSONL corpora and reports every line where the supplied identifier appears. Two scan modes: plaintext residual (mask-leak detection) and hash-mask (reusesforgelm purge's per-output-dir salt to re-derive the digest, so a purge → reverse-pii cycle for the same subject yields matching digests). Snippets are centred on the matched span and capped at 160 chars.- Default
--typeisliteral— a stray--query alice@example.commatches the literal e-mail substring, not the regex shapealice@exampleXcom. Operators wanting raw regex pass--type customexplicitly; on POSIX a 30s SIGALRM budget guards against ReDoS hangs. - New
data.access_request_queryaudit event with the identifier salted-and-hashed before emission so Article 15 access requests don't themselves leak the subject's data into the audit log.
forgelm doctor— environment pre-flight: Python version, torch + CUDA, GPU inventory, every optional extra advertised inpyproject.toml, HuggingFace Hub reachability, workspace disk space, and theFORGELM_OPERATORaudit-identity hint. Tabular text or JSON envelope (--output-format json).--offlineskips the HF Hub network probe and inspects the local cache. HonoursHF_HUB_OFFLINE=1/TRANSFORMERS_OFFLINE=1/HF_DATASETS_OFFLINE=1implicitly. Heavy deps lazy-imported inside individual probes soforgelm doctorruns on a brand-new machine.forgelm cache-models --model M [--safety S] [--output DIR]— populates the HF cache viahuggingface_hub.snapshot_download. Cache resolution:--output > HF_HUB_CACHE > HF_HOME/hub > ~/.cache/huggingface/hub. Repeatable--modelflag.forgelm cache-tasks --tasks CSV— populates the lm-eval task dataset cache vialm_eval.tasks.get_task_dict+dataset.download_and_prepare(). Requires[eval]extra.forgelm safety-eval --model <path> {--probes <jsonl> | --default-probes}— standalone counterpart to the training-time safety gate. Wrapsforgelm.safety.run_safety_evaluation; supports HF + GGUF models (GGUF requires[export]extra). Bundled probe set atforgelm/safety_prompts/default_probes.jsonl(50 prompts × 14 harm categories — controlled-substances, jailbreak, hate-speech, self-harm, csam, etc.).forgelm verify-annex-iv <path>— verifies an EU AI Act Annex IV artifact JSON: nine required field categories per Annex IV §1-9 + manifest-hash recompute (canonical-JSON SHA-256 againstmetadata.manifest_hash).verify_annex_iv_artifact(path) → VerifyAnnexIVResultexposed as a public library function.forgelm verify-gguf <path>— three-layer GGUF integrity check: 4-byteGGUFmagic header, optional metadata parse via theggufpackage, optional SHA-256 sidecar (<path>.sha256) comparison.verify_gguf(path) → VerifyGgufResultexposed as a public library function.forgelm verify-audit— verifies the integrity of the append-only audit log chain (HMAC + SHA-256).verify_audit_logis a public library function.forgelm approvals --pendinglists every run whose audit log carries ahuman_approval.requiredevent without a matching terminal decision;--show RUN_IDprints the full approval-gate audit chain plus the on-disk staging directory layout. Tabular text or JSON envelope. Defence-in-depth path-traversal guard rejects tampered staging paths outside the output directory; latest-wins semantics correctly classify re-staged runs as pending.forgelm approve <run_id>/forgelm reject <run_id>— manage the Article 14 human-approval gate.approveatomically renamesfinal_model.staging/→final_model/(with ashutil.movefallback on cross-device output mounts), emitshuman_approval.granted, and fires anotify_successwebhook.rejectrecordshuman_approval.rejectedand leaves the staging directory in place for forensic review. Approver identity resolves viaFORGELM_OPERATOR→getpass.getuser()→"anonymous".
forgelm --wizard now covers the same nine user-visible stages as
the in-browser wizard at forgelm.dev/quickstart and produces the
same generated YAML. Internal step IDs and the trainer-vs-model order
may differ between the two surfaces by design — both flows reach an
equivalent ForgeConfig shape; the divergence is documented inline at
forgelm/wizard/_orchestrator.py. The CLI flow is welcome →
use-case → model → strategy → trainer → dataset → training-params →
compliance → evaluation, with back / b to navigate backwards and
reset / r to clear in-memory state. The wizard module was split
into a sub-package (forgelm/wizard/) for orchestrator / state /
collectors / BYOD / IO concerns.
- Idempotent re-run:
forgelm --wizard --wizard-start-from /path/to/existing.yamlreads the YAML, validates it againstForgeConfigup front, and seeds the wizard with the loaded values so a bare Enter at every prompt keeps the operator's earlier answer. Save flow defaults to the same path; existing overwrite confirmation still fires. - State persistence: snapshot at
$XDG_CACHE_HOME/forgelm/wizard_state.yaml(or~/.cache/forgelm/wizard_state.yaml) saved after every completed step; resume on next launch when the snapshot is present and schema-compatible. Snapshot ischmod 0o600and cleared on successful completion. Atomic writes via temp file +fsync+os.replaceprevent half-written state under SIGKILL / power loss. - Schema-driven defaults: wizard-relevant fields in
forgelm/config.pyare flagged withjson_schema_extra={"wizard": True}. A generator script (tools/generate_wizard_defaults.py) walks the schema and writesforgelm/wizard/_defaults.json(consumed by the CLI wizard viaimportlib.resources) andsite/js/wizard_defaults.js(consumed by the web wizard'sdefaultState()). A CI guard (tools/check_wizard_defaults_sync.py) fails the run on schema- vs-shipped-JSON drift. Hardcoded fallbacks survive only when the JSON is missing entirely (broken pip install). - Trainer-specific hyperparameters:
dpo_beta,simpo_beta/simpo_gamma,kto_beta,orpo_beta,grpo_num_generations,grpo_max_completion_length,grpo_reward_modelsurface pertrainer_type. SFT short-circuits. - PEFT method breadth: the strategy step offers all four
schema-supported
lora.methodvalues (lora,dora,pissa,rslora) plus GaLore as a separate axis. All sixgalore_optimLiteral values surfaced, including the three_layerwisevariants. RoPE scaling addslongrope(full 4-of-4 schema coverage). - Compliance depth:
compliance(Article 11 + Annex IV §1) plus optionalrisk_assessment(Article 9),data.governance(Article 10),retention(GDPR Article 5(1)(e) + 17),monitoring(Article 12 + 17),evaluation.benchmark,evaluation.llm_judge,syntheticblocks all configurable from the wizard. - High-risk auto-coercion:
risk_classification ∈ {high-risk, unacceptable}automatically enablesevaluation.safety+evaluation.require_human_approvalwith a visible operator notice — front-stops the schema-sideF-compliance-110ConfigError. - Webhook URL parsing with SSRF preflight: single prompt accepts
a literal URL or
env:VAR_NAMEreference; the URL isurlparse-validated, HTTPS recommended, and loopback / RFC1918 / link-local destinations rejected up front by reusingforgelm._http._is_private_destinationso typos likehttp://10.0.0.1/xfail at config time instead of training time. - Configuration summary: the wizard prints the full generated
YAML alongside the labelled headline so the operator sees every
block (webhook / evaluation / compliance / risk / retention /
monitoring) without
cat-ing the file. - Step-diff preview: each completed step prints
+ key.path: value/~ key.path: before → afterso the operator sees exactly what changed mid-flow. - Beginner / expert toggle: beginner mode prefixes each step with a 2-3-line tutorial paragraph; expert mode is silent.
- Use-case integration: the curated quickstart-template list is
available as Step 2 of the full flow, seeding sensible defaults for
later steps without locking anything down. Use-case keys mirror
forgelm/quickstart.py::TEMPLATESexactly;quickstart.pyis the single source of truth. - Pre-flight checklist: GPU / VRAM / dataset / risk-tier signals surface before the configuration summary, calling out common operator errors (low-VRAM full-precision, missing local file, strict tier without safety eval).
- Distinct exit code for wizard cancel: new
EXIT_WIZARD_CANCELLED = 5. Clean cancels exit5instead of0so CI can distinguish "wizard finished" from "wizard never produced output". Public exit-code surface is now0–5. - Best-effort readline: arrow-key line editing + history on
Linux/macOS via stdlib
readlineimport; Windows unaffected. - Validate-on-exit: the wizard runs
ForgeConfig.model_validateon the saved YAML before declaring success. Schema rejections surface inline with the offending field rather than 30 minutes into a failed training run. - Overwrite confirmation + auto-suffix: the save flow detects
pre-existing files, asks before clobbering, and falls back to the
next free
_2.yaml/_3.yamlslot when declined. - Non-tty stdin refusal:
forgelm --wizard < answers.txtused to silently produce empty configs; the wizard now refuses to launch on a non-tty stdin and points the operator atforgelm quickstart <template>for deterministic scripted generation.
- 93-control deployer cookbook at
docs/guides/iso_soc2_deployer_guide.md(+ TR mirror) — every ISO 27001:2022 Annex A control and every SOC 2 Trust Services Criterion mapped to the ForgeLM feature that produces audit evidence. Coverage: FL (full) 11 / FL-helps 48 / OOS 34. 10-row Decision Log + 10-question deployer FAQ. - 4 new QMS docs (EN + TR mirrors):
encryption_at_rest.md(substrate-side encryption guidance per ForgeLM artefact class),access_control.md(operator identity contract;FORGELM_OPERATORform recommendations;FORGELM_AUDIT_SECRETrotation cadence),risk_treatment_plan.md(12-row ISO 27005 risk register),statement_of_applicability.md(93-control SoA matrix). Plus expansions ofsop_incident_response.md(security incidents: audit-chain integrity violation, credential leak, supply-chain CVE, webhook compromise, GDPR Art. 15/17 DSAR playbooks) andsop_change_management.md(CI gates as formal change-control mechanism). - 2 new reference tables:
iso27001_control_mapping.md(93 controls × ForgeLM evidence) +soc2_trust_criteria_mapping.md(each with TR mirror). README.md— new "ISO 27001 / SOC 2 Type II Alignment" bullet under Enterprise & MLOps with explicit "alignment, not certified" framing.
pyproject.toml— new[security]optional extra (pip-audit,bandit[toml]).tools/check_pip_audit.py+tools/check_bandit.py— JSON severity gates; HIGH / CRITICAL → exit 1, MEDIUM →::warning::, LOW silent..github/workflows/ci.yml—banditstep onforgelm/(production code only; tests/ excluded)..github/workflows/nightly.yml— newsupply-chain-securityjob runningpip-audit+banditdaily at 03:00 UTC.tools/generate_sbom.py— stdlib-only CycloneDX 1.5 emitter; called from eachcross-os-testsmatrix combo to produce a per-OS-and-Python SBOM artefact.
.github/workflows/publish.yml— tag-drivenbuild → cross-os-tests → publishchain over 3 OS × 4 Python = 12 combinations; packaged-wheel install (not editable); SBOM artifact upload per combo; OIDC trusted publishing.
.pre-commit-config.yaml— 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.
- Split-level parallelism for the audit pipeline.
--workers N(default 1) runs each split in its ownmultiprocessing.Poolworker (spawn-method, pinned in code). Speed-up scales with the number of splits —--workers 3on atrain/validation/testcorpus typically yields a near-linear speed-up. Single-split corpora ignore values >1. - Determinism contract pinned by tests. The merge step that
builds the final report stays single-threaded so
data_audit_report.jsonis byte-identical across worker counts (onlygenerated_atdiffers as expected — stripped textually before SHA-256 comparison). - CLI exposure.
forgelm audit --workers Nregistered on the audit subparser with a new_positive_intargparse type validator (rejects 0 / negatives at parse time).
- 50 new doc files across guides + reference + usermanuals
(EN + TR mirrors). Highlights:
- 11 new reference docs covering every shipped subcommand
(
verify_audit,verify_annex_iv_subcommand,verify_gguf_subcommand,purge_subcommand,reverse_pii_subcommand,approve_subcommand,approvals_subcommand,doctor_subcommand,cache_subcommands,safety_eval_subcommand,library_api_reference). - 5 new guides:
getting-started.md(v0.5.5 onboarding canonical, opens withforgelm doctor);air_gap_deployment.md(deep deployer cookbook forcache-models/cache-tasks);human_approval_gate.md;library_api.md;performance.md. - 9 new usermanual pages:
compliance/{verify-audit, annex-iv, gdpr-erasure, human-approval-gate},deployment/verify-gguf,operations/{iso-soc2-deployer, supply-chain},reference/{library-api, performance}.
- 11 new reference docs covering every shipped subcommand
(
- Bilingual TR mirror sweep across
docs/qms/*.md(10 mirrors) and 4 doc pairs brought to structural parity with their EN originals. - i18n parity: German / French / Spanish / Chinese now match
English and Turkish at 731 keys each (was 689). The 168
previously-untranslated strings cover the regulator-facing surfaces
(
compliance.gdpr15.*,compliance.gdpr17.*,compliance.iso.*,features.gov.*,features.eval.safetyeval.*,features.ent.*). docs/reference/audit_event_catalog.md(+ TR mirror) — comprehensive event vocabulary with payload schemas.docs/standards/release.md— "Deprecation cadence" section.
tools/check_bilingual_parity.py --strict— replaces the inline H2-only check inci.ymlwith extended H2 + H3 + H4 structural diff. Detects missing sections, depth changes, reorders. Pair registry now 40.tools/check_anchor_resolution.py --strict— markdown link resolution guard; baseline cleanup brought broken anchors from 36 → 0.tools/check_cli_help_consistency.py --strict— discovers the live parser surface by spawningforgelm <subcommand> --helpfor every shipped subcommand; walks docs / README for fenced bash/shellforgelminvocations; reports drift classes (subcommand not in parser; flag not in parser; flag value not in parser'schoiceslist).tools/check_field_descriptions.py --strict forgelm/config.py— AST-based scanner of PydanticBaseModelsubclasses; exits 1 on any field missing adescription=.tools/check_no_analysis_refs.py— prohibits citations to gitignored working-memory directories from the public tree.tools/check_wizard_defaults_sync.py— fails CI on schema-vs- shipped-JSON drift for the wizard's source-of-truth defaults.
forgelm/_http.safe_get(url, *, headers, timeout, ..., method="GET"|"HEAD")— disciplined outbound GET / HEAD mirroringsafe_post's policy contract (scheme allowlist, SSRF guard, timeout floor, redirect refusal, secret-mask error path, TLS verify). Used byforgelm doctorand any future probe / telemetry / registry ping that needs an outbound read.forgelm/_http.safe_post— single boundary for outbound HTTP with SSRF guard, redirect refusal, scheme policy, timeout floor, TLS pinning, secret-mask error reasons. Migrated webhook + judge + synthetic call sites..github/workflows/ci.ymllint-http-disciplinestep grepsforgelm/forrequests.*(...),urllib.request.urlopen(...),httpx.*(...)outsideforgelm/_http.pyand fails on any hit.
- When
evaluation.require_human_approval=true, the trainer now saves the final adapters tofinal_model.staging/instead of writing tofinal_model/before review. Thehuman_approval.requiredaudit event payload now also carriesstaging_pathandrun_idso downstream tooling can cross-check the approval against the originating run. evaluation.staging_ttl_daysconfig field documents the retention horizon forfinal_model.staging/after aforgelm reject; default 7 days. Auto-deletion enforcement is delegated toforgelm purge(GDPR right-to-erasure tooling).
- Erasure family (six events):
data.erasure_requested,data.erasure_completed,data.erasure_failed, plus three operator-warning events. - Access-request family:
data.access_request_query(Article 15) — identifier salted-and-hashed before emission. - Cache-population family (six events):
cache.populate_*. - Approval family:
human_approval.required,human_approval.granted,human_approval.rejected,approval.required. - Training lifecycle:
training.revertedpaired withWebhookNotifier.notify_reverted. - Operator-error family:
audit.classifier_load_failed,cli.legacy_flag_invoked.
SafetyEvalThresholdsdataclass bundles five Phase 9 knobs sorun_safety_evaluationstays under the 13-param ceiling.tests/test_lazy_imports.py— regression test pinning thatimport forgelm.trainer/import forgelm.modeldo not eagerly load torch.
forgelm/cli.py→forgelm/cli/package. The ~2300-line monolith was split into a 24-module package (subcommands/,_dispatch,_training,_dry_run,_result,_resume,_logging,_exit_codes, etc.). Theforgelm.cli:mainentry point andpython -m forgelm.cliare preserved; dispatcher uses late-binding facade re-resolution so test monkeypatches (forgelm.cli._run_chat_cmdetc.) keep resolving correctly.forgelm/data_audit.py→forgelm/data_audit/package. 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). The publicforgelm.data_audit.Ximport surface — including the test-touched private helpers — is preserved by__init__.pyre-exports so external callers and the test suite keep working without code changes.- Pydantic
description=migration — every Pydantic field across the config schema (19 model classes) migrated toField(default=..., description=...)form. Operator-facing copy pulled from existing inline comments + variable semantics; the configuration reference can now be auto-generated from the schema in lockstep with the code. - Site copy now matches the live code surface. Sweep across
site/*.htmlandsite/js/translations.jscorrecting drift that had accumulated against the production code:- YAML demo accuracy: the homepage hero YAML demo and the
quickstart
verify-auditexample now use real Pydantic field names and accept paths that pass the live CLI check; copying any visible snippet and runningforgelm --config <copy> --dry-runworks as advertised. - Compliance artefact tree: the EU AI Act / ISO 27001 page
redrawn against the actual on-disk layout —
compliance/sub-tree (wasartifacts/),audit_log.jsonlat the checkpoint root,final_model/carrying the model card + deployer instructions + integrity manifest. Removed phantomconfig_snapshot.yamlrow (no code path emits it). - Ghost YAML keys removed:
compliance.config_hashandcompliance.human_approval(whichComplianceMetadataConfig'sextra="forbid"rejected) replaced with the canonicalevaluation.require_human_approvaland a description offorgelm verify-auditchain integrity. - Ghost CLI flag removed:
--model-cardno longer mentioned on the Article 13 evidence cell; the model card is auto-generated on every successful run. - Stale namespace + symbol-list corrected: Library API
references now use
from forgelm import …with real symbols fromforgelm.__all__(ForgeTrainer,audit_dataset,verify_audit_log,verify_annex_iv_artifact,mask_pii). - Wording aligned with live behaviour: auto-revert (deletes
artefacts + exits with
EXIT_EVAL_FAILURE = 3, not "rolls back to last-good checkpoint"), exit codes (0–5, was0/1/2/3/4), Annex IV "nine §1-9 sections" (was "eight"),forgelm safety-evalaccepted formats (--probesJSONL or--default-probes; outputssafety_results.json+safety_trend.jsonl),forgelm verify-annex-ivclaim narrowed to schema completeness +manifest_hash(audit-chain integrity is the separateverify-auditcommand),forgelm purgedocumented as emittingdata.erasure_requested+data.erasure_completedaudit events. - Wizard preset divergence: the in-browser
USE_CASE_PRESETSis now byte-equivalent toforgelm/quickstart.py::TEMPLATES. Operators who finish the wizard see the same model IDs and bundled-dataset paths the CLIquickstartwould have set. - JSON-LD
operatingSystem: all 8 pages updated from"Linux, macOS"to"Linux, macOS, Windows", matching the README's tri-platform pitch and the PyPIOperating System :: OS Independentclassifier. - Mermaid disclosure: the privacy page now lists three third-party requests (Google Fonts, jsDelivr / Mermaid on the Guide page only, Formspree on contact-form submit) in all six locales.
- Audit-event scope: Article 12 description widened from "training start, eval gates, auto-revert decisions" to the real vocabulary (training start/end, eval gates, auto-revert decisions, human-approval gates, GDPR Article 17 erasure, Article 15 access-request queries, model export) with HMAC + SHA-256 chain language replacing the per-artefact SHA-256 wording.
- YAML demo accuracy: the homepage hero YAML demo and the
quickstart
- Working-memory directories cleanup. Operator-local research,
audit drafts, and external-repo comparisons now live in
working-memory directories that are strictly gitignored with no
exceptions and never appear in fresh clones. The previous
re-include carve-outs that exposed individual files were dropped,
the working-memory tree was untracked at the directory level, and
every public-tree citation pointing into it was rewritten or
removed. The rule is now codified in
docs/standards/documentation.md("Working-memory directories"), enforced by a new CI guard (tools/check_no_analysis_refs.py) wired into the self-review chain inCLAUDE.md. False positives (functional path filters in production code) are handled via an_EXEMPTallowlist with per-entry justification comments. - Minimum required
torchbumped from 2.1.0 to 2.3.0.torch.distributed.fsdp.FSDPModule(introduced in torch 2.3) is referenced bytests/test_grpo_reward.pyand runtime GRPO paths. - 16 broad
except Exceptionsites narrowed across_streaming.py,trainer.py,safety.py,judge.py,compliance.py,ingestion.pyto specific exception classes; 7 sites retained with# noqa: BLE001and rationale comments perdocs/standards/error-handling.mdcarve-out. MoE expert-name resolver migrated from hardcoded substring match to regex-registry (_EXPERT_NAME_PATTERNS) covering Mixtral, Qwen 3 MoE, DeepSeek-V3, Phi-MoE. - 6 enum-shaped config fields tightened to
Literal[...]—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. AuditLogger— operator identity raisesConfigErrorinstead of falling back to literal"unknown".getpass.getuser()@socket.gethostname()chain withFORGELM_ALLOW_ANONYMOUS_OPERATOR=1escape hatch.AuditLogger.log_event—os.fsync(f.fileno())after flush; chain durability across power-cut.compute_dataset_fingerprintsplit into three helpers (local file / HF metadata / HF revision); HF Hub revision SHA pinned.- Safety eval batched with token-pad-longest + per-batch CUDA-OOM
fallback —
_generate_safety_responsesand_generate_responses_batchedusebatch_size=8default with single-prompt fallback on OOM. Per-batch error handling extracted to_generate_*_batch_with_oom_retryhelpers. _chunk_paragraph_tokens— single-encode + offset slicing (performance fix)._post_payload— delegates tosafe_postwithmin_timeout=1.0for back-compat.- 7 notebooks install from PyPI (
forgelm[qlora]==0.5.5) instead ofgit+https://.... - CI enforces
pytest --cov-fail-under=40viapyproject.tomladdopts. Matrixfail-fast: false;usermanuals-validate.ymlruns on push + PR. - Site honesty:
compliance.htmlartefact tree,quickstart.htmltemplate names, GPU stat (16 vs claimed 18) — all refreshed against real code. - QMS
sop_data_management.md— single v0.5.0 story; v0.5.1+ / v0.5.2 splits removed. - Roadmap (
roadmap.md,roadmap-tr.md,releases.md) — v0.5.5 marked released; tristate status legend added. - Webhook config persisted into compliance manifest.
generate_training_manifestnow writeswebhook_configinto<output_dir>/compliance/compliance_report.jsonsoforgelm approve/forgelm reject(which run with no--configflag) can rebuild the notifier and fire the success / rejection webhook. Operator secrets resolve from env at runtime viaurl_env/secret_envso persisting the config shape is safe. forgelm.webhookexports_is_private_destinationvia__all__for back-compat (helper moved toforgelm._http).- Standards updates —
architecture.mdreflects the package splits;documentation.mdcites the new CI guards;localization.mdformalises EN + TR mandatory + DE / FR / ES / ZH deferred;release.mddocuments the v0.5.5 release sequence;testing.mdtest count refreshed.
- Nightly pip-audit gate —
transformersCVE-2026-1839 (issue #37). The Supply-chain security workflow flagged the CVE whose published fix lives intransformers 5.0.0rc3(release candidate). ForgeLM'spyproject.tomlpinstransformers>=4.38.0,<5.0.0; the 5.x branch is a major version bump that breaks downstream callers (TRL adapter signature changes + tokenizer-config API drift), and there is no 4.x backport available at the time of writing. Stop-gap: an explicit--ignore-vuln CVE-2026-1839in.github/workflows/nightly.ymlwith documented rationale and a remove-after condition (revisit at every release; remove the ignore oncetransformersships a 4.x point release with the fix or ForgeLM cuts a tracked major-version-bump cycle). - Wizard YAML output is now
safe_dump(allow_unicode=True)with explicit UTF-8 file handles — prevents!!python/objecttags from leaking into generated YAML when a collector returns a Path or set, and stops mojibake on non-ASCII compliance fields. - Wizard hardware-detection cache.
_detect_hardware()runs once per session; the welcome step and the post-save pre-flight checklist share the result instead of paying a second torch import- CUDA enumeration (~50–200 ms saved).
- Wizard back / reset semantics.
WizardBackrestoresstate.configfrom acopy.deepcopysnapshot taken before the step ran (partial mutations no longer leak into the previous step's prompts).WizardResetre-loops with a fresh state instead of treating the reset as a completed run and trying to save an empty config. - Wizard BYOD path validation. Typed dataset values are now
checked as a directory of ingestible docs, a JSONL/JSON file, or
an HF Hub ID before being accepted; bare typos no longer silently
become
data.dataset_name_or_path. - Wizard non-empty re-prompt for Article 9 / Article 10 free-text
fields (
intended_use,foreseeable_misuse,mitigation_measures,collection_method,annotation_process,known_biases). Empty values used to slip through and surface as PydanticConfigErrorat training-time load. - CUDA capability check.
torch.cuda.get_device_properties(0).total_mem→total_memoryin_detect_hardware(the previous attribute doesn't exist; welcome step crashed on real CUDA hosts). - Wizard cross-tab sync. The web wizard listens for
storageevents and reloads state when a sibling tab edits the same wizard, eliminating the "last write wins" race. - Wizard bundled safety-probes resolution. The wizard's default
safety probe set is now resolved through
importlib.resources.files("forgelm.safety_prompts"), fixing thepip install forgelmregression whereconfigs/safety_prompts/general_safety.jsonlwas the wizard default but never shipped in the wheel. forgelm.compliance.verify_audit_logas a public function — closes a critical gap where the chain integrity check existed but had no library entry point.- Audit event catalog and CLI sample drift fixed — placeholder
<TBD>entries inaudit_event_catalog.mdfilled; trailing-whitespace cleaned; CLI help sample indocs/reference/usage.mdbrought in sync with current subcommand surface. - Tier 1 ghost-feature drift:
verify-log→verify-auditrename in user manuals.forgelm chatslash commands aligned with parser: removed/load,/top_p,/max_tokens,/safety on|off;/quit→/exit.q6_kquant level removed from GGUF docs (parser only supportsq2_k|q3_k_m|q4_k_m|q5_k_m|q8_0|f16);f16row added.FORGELM_RESUME_TOKENenv var removed from manuals; replaced with the canonical CLI subcommand flow (forgelm approve/reject+forgelm approvals --pending).FORGELM_CACHE_DIRenv var removed;HF_HOMEdeclared canonical.forgelm benchmark --model "..."(subcommand form that doesn't exist) →forgelm --config <yaml> --benchmark-only <path>(the shipped flag form).--export-bundle→--compliance-exportrename.kserve/tritonrows removed from deploy targets (parser only ships{ollama,vllm,tgi,hf-endpoints}).- Ingest flag drift:
--max-tokens→--chunk-tokens; removed non-existent--language/--include/--exclude/--format/--pii-localeflags;--strategychoices clarified to{sliding,paragraph,markdown}.
- Documentation and CI guard plumbing:
docs/design/wizard_mode.mdrewritten to describe the actual 9-step flow;docs/reference/architecture{,-tr}.mdheading updated fromwizard.pytowizard/to match the sub-package layout;docs/usermanuals/{en,tr}/compliance/human-approval-gate.mdcross-references exit code 5 alongside the existing exit-4 discussion.
forgelm --data-audit PATH— the legacy flag now emits aDeprecationWarningand ancli.legacy_flag_invokedaudit-log event on every invocation. Behaviour is unchanged; the flag is scheduled for removal in v0.7.0. Migrate to theforgelm audit PATHsubcommand (same output, same exit codes). See docs/standards/release.md for the removal timeline.evaluation.staging_ttl_daysalias-forwards toretention.staging_ttl_dayswith a singleDeprecationWarning; conflicting values raiseConfigError. Removal scheduled for v0.7.0.
[ingestion-secrets]extra (detect-secrets>=1.5.0,<2.0.0) — reserved during Phase 12 for a follow-up integration that never landed. Thedetect-secretsscanner expects file paths while ForgeLM audits row-level JSONL streams, so the wire-up was rejected as architecturally incompatible. The prefix-anchored regex set inforgelm/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 frompyproject.toml, the install snippet fromREADME.md, and the "fallback regex set" framing from the secrets module docstring.
- High-risk + safety-disabled now raises
ConfigError.risk_classification ∈ {high-risk, unacceptable}combined withevaluation.safety.enabled=falsenow raises at config-load time (was a warning). EU AI Act Article 9 risk-management evidence cannot be derived from a disabled safety eval. Operators with sandboxed runs must lower therisk_classificationor enable safety. WebhookConfig.timeoutdefault raised 5s → 10s. Slack/Teams gateway latency spikes regularly cross 5s; webhook failure is best-effort but a timeout silently degrades the audit chain.
Theme: "Document Ingestion + Data Curation Pipeline" — Phases 11, 11.5, 12, and 12.5 ship as one comprehensive release.
Note on consolidation. Originally planned as four sequential PyPI tags (
v0.5.0/v0.5.1/v0.5.2/v0.5.3) but consolidated into a singlev0.5.0because the four phases form one coherent surface (ingest → polish → mature → polish) that's hard to use in parts. Git history retains the four phases as separate commit batches; this entry collapses them into the user-facing release notes. Section markers below preserve the phase boundary so reviewers can map back to docs/roadmap/releases.md.
The release adds:
- Phase 11 —
forgelm ingest(PDF / DOCX / EPUB / TXT / Markdown → SFT-ready JSONL) +forgelm audit(length / language / near-duplicate / cross-split leakage / PII) + EU AI Act Article 10 governance integration. - Phase 11.5 — operational polish on the Phase 11 surface: LSH banding, streaming reader, token-aware chunking, PDF header/footer dedup, PII severity tiers, atomic audit writes.
- Phase 12 — data curation maturity: MinHash LSH dedup option, markdown-aware splitter, code/secrets leakage tagger, heuristic quality filter, DOCX table preservation.
- Phase 12.5 — small additive polish:
--all-maskshorthand, Croissant 1.0 dataset card emission, optional Presidio ML-NER PII adapter, wizard "audit first" entry point.
CI / docs / standards bookkeeping accompanying every phase is folded into "Cross-cutting review hardening" at the bottom (rounds 1–12 of review-cycle fixes applied across the four phases above).
Four follow-up items from
docs/roadmap/completed-phases.md
ship together — none require new architecture; each is a small
additive surface on top of the Phase 12 ingestion + audit lineage.
forgelm ingest --all-mask(item #3) — one-flag shorthand for--secrets-mask --pii-maskin the documented mask order (secrets first so combined detectors don't double-count overlapping spans). Composes additively with explicit flags (set-union, no error). Pure UX; no new behaviour.forgelm audit --croissant(item #2) — opt-in Google Croissant 1.0 dataset card emitted under a newcroissantkey indata_audit_report.json. The card carries dataset-level identity, onecr:FileObjectper JSONL split, and acr:RecordSetper split withcr:Fieldentries derived from the audit's column detection. Existing audit JSON keys are byte-equivalent — the block stays empty when the flag is off (same precedent assecrets_summary/quality_summary). Lets the same JSON file double as both the EU AI Act Article 10 governance artifact and a Croissant-consumer dataset card.urlandcontentUrluse the as-typed input string and the relative split filename, never the resolved absolute filesystem path, so cards published to HuggingFace / MLCommons don't leak the auditor's local layout.- Croissant
version(sc:version, dataset version) is omitted deliberately — the audit doesn't have first-class evidence for it; vocab conformance is declared viaconformsTo. Operators that publish hand-editversionlike they dolicense/citeAs. - The card is now also surfaced in the
--output-format jsonstdout envelope alongside the on-disk report so CI consumers don't need a second file slurp.
forgelm audit --pii-ml [--pii-ml-language LANG]+ new[ingestion-pii-ml]extra (item #1) — opt-in Presidio ML-NER PII detector layered on top of the existing regex detector. Adds the unstructured-identifier categories the regex inherently misses (person,organization,location) into the samepii_summary/pii_severityblocks under disjoint category names. Severity tiers in the newPII_ML_SEVERITYtable:person → medium,organization → low,location → low(deliberately below the regexcritical/hightiers because NER false-positive rates are materially higher than regex-anchored detection). The pre-flight check covers BOTH the missing-extra branch AND the missing-spaCy-model branch —presidio-analyzerdoes not transitively ship a spaCy NER model, so the install recipe is now two lines:Without either,pip install 'forgelm[ingestion-pii-ml]' python -m spacy download en_core_web_lgforgelm audit --pii-mlraisesImportErrorwith the recipe before any rows are scanned. Per-row Presidio failures are scoped to(ValueError, RuntimeError)so a single malformed row never blocks the audit, but a deepOSErrorfrom a missing model surfaces loudly instead of silently scoring zero ML coverage.--pii-ml-language(default"en") lets non-English corpora point at the matching spaCy model; Presidio raises a typed exception when no engine is registered for the requested language.- Wizard "audit first" entry point (item #4) — when the wizard
resolves a JSONL (either typed directly or produced by the
Phase 11.5
_offer_ingest_for_directoryingest flow), it now offers to runforgelm auditon it inline and printssummarize_report's verdict before continuing. Mirrors the_offer_ingest_for_directoryshape exactly. Closes the BYOD audit loop end-to-end. Audit is informational, not a gate — failures fall through to the "continue without audit" path.
Touch points (so the next reviewer can audit blast radius quickly):
forgelm/ingestion.py— no module changes (the flag composes at the CLI boundary into the existingpii_mask/secrets_maskbooleans).forgelm/cli.py— three new flags on the existing subparsers (--all-maskonforgelm ingest;--croissantand--pii-mlonforgelm audit); dispatcher signatures threaded through.forgelm/data_audit.py—_HAS_PRESIDIOsentinel,_require_presidio,_get_presidio_analyzer(cached),detect_pii_ml,PII_ML_SEVERITY,PII_ML_TYPES,_PRESIDIO_ENTITY_MAP,_build_croissant_metadata,_CROISSANT_CONTEXT. Newenable_pii_ml/emit_croissantparameters onaudit_dataset/_process_split/_audit_split; newenable_pii_mlfield on_StreamingAggregator; newcroissantfield onAuditReport._build_pii_severitynow consults the mergedPII_SEVERITY ∪ PII_ML_SEVERITYtable.forgelm/wizard.py— new_offer_audit_for_jsonl(path)helper; invoked from_offer_ingest_for_directory(after ingest produces a JSONL),_validate_local_jsonl(after a directly-provided JSONL passes validation), and_prompt_dataset_path_with_ingest_offer(after a non-directory JSONL is provided to the full wizard).pyproject.toml— new[ingestion-pii-ml]extra (presidio-analyzer>=2.2.0,<3.0.0).tests/test_phase12_5.py— 11 new tests, four classes (one per backlog row).tests/test_wizard_byod.py— three existing tests get an extra"n"answer to decline the new audit-first offer (the offer behaviour has its own coverage intest_phase12_5.py).- Docs —
README.mdinstall matrix + Phase 12.5 feature line;docs/standards/architecture.mdextras matrix;docs/guides/ingestion{,-tr}.mddocs/guides/data_audit{,-tr}.mdget dedicated sections per feature;notebooks/data_curation.ipynbmentions--all-maskand the Phase 12.5 audit add-ons inline.
Inline-comment batches landing on top of PR #13 (now merged to main).
Same review surface as rounds 4-7; further hardening on top of the
v0.5.2 content.
- Audit log hardening (
forgelm/compliance.py) — HMAC_hmacfield is now emitted only whenFORGELM_AUDIT_SECRETis set; without a secret, a key derived solely from the publicrun_idwould be forgeable, so we no longer claim tamper-evidence we cannot deliver.log_eventre-reads the chain head from disk under the sameflockso two writers sharing the same log can no longer fork the chain._read_chain_headrefuses to derive a head from a tail that does not end with\n(truncated last record). The oversize- final-entry case is recovered by re-reading fromseek_startwithout skipping the partial first line. - Deployer-instructions Markdown injection (
forgelm/compliance.py::generate_deployer_instructions) — config-derived strings (system_name,model.name_or_path, fine-tuning method, model location, foreseeable-misuse bullets, metric names) now go through_sanitize_mdbefore template substitution; pipes / backticks / link syntax in any of those can no longer break out of table cells or bullets in the generated Article 13 document. - Quality-filter denominator (
forgelm/data_audit.py::_build_quality_summary) —overall_quality_scorenow divides by the number of rows the filter actually evaluated (text-bearing dict rows) instead oftotal_samples. A corpus that's 50 % null but 100 % clean on the rest now reads1.0instead of0.5. - NumPy-fast-path bits guard (
forgelm/data_audit.py::compute_simhash) — the_compute_simhash_numpydispatch now also gates onbits <= 64; without it,np.uint64would silently truncate digests wider than 64 bits. - Sliding-overlap clamp (
forgelm/ingestion.py::ingest_path) — when--overlapis not passed and the strategy issliding, the implicitDEFAULT_SLIDING_OVERLAP(200) is now clamped tochunk_size // 2. A small--chunk-size 300used to trip_chunk_sliding's "overlap > chunk_size // 2" guard with the default overlap — surfacing as a confusing error for a knob the user did not set. - Batch-tokenizer narrow except (
forgelm/ingestion.py::_count_section_tokens) — the bareexcept Exceptionaround the batchedtokenizer(blocks)call is now narrowed to(TypeError, ValueError)(the documented unsupported-batch signal); the returnedBatchEncodingis shape- validated before itsinput_idsis consumed. Real bugs (corrupted input, OOM, etc.) no longer mask behind the slow per-block fallback. - Webhook secret-fallback safety (
forgelm/webhook.py) —requests.postnow passesallow_redirects=False(an SSRF-pre-validated URL cannot be redirected to a private destination) and themask_secretsImportErrorfallback emits[REDACTED — secrets masker unavailable]instead of the raw 512-char reason prefix. See #14 for the remaining DNS-rebinding TOCTOU follow-up tracked forv0.5.3. - Trainer governance failure visibility (
forgelm/trainer.py) — thedata_governance_report.jsonexport try/except now catches the fullExceptionset (wasOSErroronly) so non-IO failures (TypeError,ValueError,AttributeError) still surface ascompliance.governance_failedaudit events instead of crashing the surrounding compliance flow. The rollupcompliance.artifacts_exportedevent is gated on agovernance_okflag so the audit chain truthfully reflects which artefacts are actually on disk. - Compliance manifest exception narrowing (
forgelm/compliance.py) — the broadexcept Exceptionaround the HF Hubload_dataset_builderfingerprint fetch is now a tuple of realistic failure modes (ImportError,FileNotFoundError,ValueError,AttributeError,ConnectionError,TimeoutError). - Strict messages-format validation (
forgelm/data.py) —_process_messages_formatnow explicitly checksisinstance(role, str)andisinstance(content, str)before formatting; non-string content (dicts, ints) used to be silently coerced via f-string__format__and slip through into training. - Wizard ASCII regex flag (
forgelm/wizard.py) —_HF_HUB_ID_REnow compiles withre.ASCIIso the\wclass means[A-Za-z0-9_]. HF Hub IDs are ASCII-only, and Unicode-aware\wwould otherwise accept characters the Hub itself rejects. - GGUF converter case-insensitive validation (
forgelm/export.py) — theFORGELM_GGUF_CONVERTER.pysuffix check now usescasefold()(cross-platform: HFS+/NTFS), andexport_model()'s catch widened from(ImportError, FileNotFoundError)to also includeValueErrorso a non-.pyenv override produces anExportResultinstead of crashing the caller. - Markdown chunker complexity refactor (
forgelm/ingestion.py) —_chunk_markdown_tokenssplit into_build_markdown_section_blocks(render breadcrumb + body),_count_section_tokens(batch tokenizer call with per-block fallback), and the main chunker (greedy packing). Cognitive complexity drops from 16 → ~8. - Bidirectional MinHash extraction (
forgelm/data_audit.py) — the two near-identicala→lsh_b/b→lsh_aquery loops in_count_leaked_rows_minhash_bidirectionalwere extracted into one_count_leaks_against_indexhelper. Complexity drops from 24 → ~5; the SonarCloud duplication metric on this file goes away. - Streaming length digest (
forgelm/data_audit.py) — the per-split text-length distribution is now accumulated via a bounded_LengthDigest(Algorithm R reservoir, 100K cap) instead of an unboundedList[int]. Audit memory on multi-million-row splits is now O(1) instead of O(n). - Documentation drift sweep (round N) — five compliance-summary
links repointed to
../../forgelm/..., two missing FSDP knobs (fsdp_backward_prefetch,fsdp_state_dict_type) and two webhook knobs (allow_private_destinations,tls_ca_bundle) added to both EN and TRconfigurationreference; Pro CLI section added toREADME.md; CI now runs a bilingual H2 parity check across seven EN/TR doc pairs (configuration,usage,distributed_training,data_preparation,architecture,ingestion,data_audit); test count refreshed to 47 inCONTRIBUTING.mdto matcharchitecture.md; secrets list aligned to the full nine families inforgelm.data_audit.SECRET_TYPES(was missing two private-key splits + Azure storage in some prose). - Phase 12 fenced log block in both
usage.mdandusage-tr.mdnow uses```textso markdownlint MD040 stops flagging it.
Multi-dimension review (business, code, compliance, documentation, performance, security) surfaced a cluster of correctness, claim/evidence, and silent-failure issues that have been swept in batches.
- Version drift —
forgelm.__version__was hard-coded to0.5.0rc1inforgelm/__init__.pywhilepyproject.tomldeclared0.5.2rc1. The literal is now derived from the installed distribution viaimportlib.metadata.version("forgelm")(with a0.0.0+devfallback for raw source checkouts), andcompliance._get_version()follows the same resolution path so audit / Annex IV manifests stamp the correct producer version. - Audit log integrity (
forgelm/compliance.py::AuditLogger) —_load_last_hashpreviously re-rooted the chain to"genesis"on any read failure with only alogger.debugmessage;log_eventadvanced_prev_hashbefore the file write and swallowed write failures withlogger.warning. Both paths now distinguish file-missing from file-unreadable, raise on real I/O errors, and only advance the hash chain after a successful write. compute_dataset_fingerprintTOCTOU —@lru_cache(maxsize=32)keyed on the path string only would return stale fingerprints when the file was rewritten in place. Cache dropped; symlinks resolved before hashing;os.stat()now captured atomically alongside the SHA-256 stream so size/mtime cannot drift between the two reads.generate_data_governance_reportwiring — defined and tested but never called from production code. Now invoked by_export_compliance_if_neededsodata_governance_report.jsonactually lands in the trainer'soutput_dirper EU AI Act Article 10.- Silent-failure sweep — replaced
except Exception:swallows with concrete-class catches + log + raise/sentinel:data.py::_process_messages_format(catches malformed message rows by exception class, raises an explicitValueError),safety.py::_release_model_from_gpu(RuntimeError/OOMonly),cli.py::_load_config_or_exit(splityaml.YAMLError+pydantic.ValidationErrorfor clearer error messages),config.py::ForgeConfig.load_config(specific Pydantic / YAML branches). - Pydantic schema discipline — six bare-
strfields (trainer_type,merge.method,model.backend,distributed.fsdp_strategy,risk_assessment.risk_category,monitoring.metrics_export) converted toLiteral[...]so JSON Schema / IDE auto-complete surfaces the allowed values; redundant runtime validators dropped. - Webhook hardening —
forgelm/webhook.pynow refuses non-loopback private destinations without explicit opt-in (webhook.allow_private_destinations), runs the failure-reason payload throughmask_secrets, passesverify=Trueexplicitly torequests.post, and rejectstimeout < 1. - Performance —
forgelm/trainer.pylazy-importstorch/transformers/trlinto method bodies, dropping CLI cold-start cost by ~700-1500 ms onforgelm auditandforgelm --help. Audit'sagg.minhashesis no longer copied vialist(...)before LSH (saves ~1 GB on 1M-row splits). - Documentation — refreshed module / test / notebook counts in
CONTRIBUTING.mdanddocs/reference/architecture.md; addedforgelm/templates/to the directory layout. Removedforgelm chat --safetyfromusage.md(flag does not exist incli.py).coverage.fail_underindocs/standards/testing.mdnow matchespyproject.toml(40, not 25).
SonarCloud python:S5852 flagged _MARKDOWN_CODE_FENCE (forgelm/ingestion.py L515) — the regex ^ {0,3}(?P<fence> `{3,}|~{3,})(?P[^\n]*)$had **two unbounded greedy quantifiers in sequence over overlapping character classes** (the fence run is `` `` / ~; the `rest` capture's `[^\n]` includes both fence chars), the textbook polynomial-runtime shape per regex.md rule 4.
- Empirically linear in CPython (50K-char pure-backtick run = 16 μs), but the static analyser can't prove that — and we already use non-regex line walkers everywhere else for markdown parsing (regex.md rule 6).
- Replaced with
_parse_md_fence(line)— a non-regex parser that returns(fence_char, run_length, rest_after_run)orNone. Provably O(n) per line; 100K-char pure-backtick run measures ~10 μs. _markdown_sectionsupdated to use the helper directly (no behavioural change — the helper returns the same tuple shape the regex's named groups did).- 2 new regression tests in
tests/test_phase12_review_fixes.py::TestRegexLinearity—test_parse_md_fence_linear_on_long_runs(≤ 100 ms cap on N=100K) +test_parse_md_fence_behaviour(pinned outputs for opener with info string, 4-char fence, 2-space indent, 4-space indent → None, sub-3-char run → None, mismatched chars after run).
Round-3 review caught two real correctness bugs (Unicode \w in
secret regexes, fence-length rule violation in markdown / code-fence
tracking) plus a handful of doc / fixture parity issues. All applied.
re.ASCIIflag on secret regexes (forgelm/data_audit.py) — Last commit changed[A-Za-z0-9_-]→[\w-]ingithub_token/openai_api_key/google_api_key/jwt, but Python's default\wis Unicode-aware (matchesünicode,türkçe, …), which would broaden the match universe to include non-ASCII chars that real credentials never contain. Addedflags=re.ASCIIto all four patterns so\wis restricted to ASCII. Patterns that already use explicit ASCII character classes (aws_access_key,slack_token, the explicit[A-Z0-9]ones) are unchanged.regex.mdRule 1 corrected — Previous wording stated[A-Za-z0-9_]and\ware equivalent in Python. They are not. Rewrote the rule with a side-by-side example showing the Unicode / ASCII divergence, plus a decision table: ASCII-only inputs →\wwithre.ASCII(or explicit class), natural-language inputs → bare\w(Unicode-aware), mixed → be explicit.- CommonMark fence-length rule enforced (
forgelm/data_audit.pyforgelm/ingestion.py) — CommonMark §4.5 requires the closing fence to use at least as many fence characters as the opener. Both_strip_code_fencesand_markdown_sectionspreviously tracked only the fence character, so a 4-backtick opener (````) was prematurely closed by a 3-backtick line._is_code_fence_opennow returns(char, run_length);_is_code_fence_closeaccepts the minimum run-length and rejects shorter closes. The markdown splitter's_MARKDOWN_CODE_FENCEregex captures the fence run ((?P<fence>...)) and the rest of the line ((?P<rest>...)) so the splitter can also enforce "no info string on close" alongside the length rule. All three CommonMark §4.5 close-side rules (matching char + run length ≥ open + no info string) now hold.
data_audit.mdreframes[ingestion-secrets]— The doc previously implied installing the extra layereddetect-secretson top of the regex fallback. The current code does not invokedetect-secretsat all. Reworded as forward-compatibility: installing the extra is safe to pin in requirements files but doesn't change audit behaviour today.READMEclarifiessemanticchunking strategy — Listed as reserved/planned: the implementation raisesNotImplementedErrorand the CLI hides it from--strategychoices. Previous wording implied it was available at runtime.ingestion-tr.mdCLI synopsis adds Phase 12 flags —--strategy markdownand--secrets-masknow appear in the options block; short Turkish description for each.review-prskill heading updated — "The six-question review" → "The seven-question review" to match the regex-check question added in the previous commit.data_curation.ipynbfixture credentials fragmented —deploy_runbook.txtfixture now buildsAKIA…/ghp_…strings at runtime from inert fragments (same convention astests/test_data_audit_phase12.py::FAKE_AWS_KEY). Repo-wide secret scanners no longer flag the notebook source.data_curation.ipynbMinHash install uses the project extra —pip install 'datasketch>=1.6.0,<2.0.0'→pip install 'forgelm[ingestion-scale]==0.5.2'so the recipe matches the install hint baked intoforgelm.data_audit._require_datasketch.TestMinHashDistinctSemanticuses pytest'stmp_path— Was creating a directory undertests/which mutated the repo and broke parallel pytest runs. Now uses the standardtmp_pathfixture; no manual cleanup needed.- 3 new fence-length regression tests in
tests/test_phase12_review_fixes.py::TestFenceRunLengthRule: 4-backtick block not closed by 3 backticks;_strip_code_fencesrespects the length rule; close lines with info strings are treated as content (CommonMark §4.5 conformance).
- New standard
docs/standards/regex.md— codifies 8 hard rules absorbed from Phase 11/11.5/12 review cycles (no[A-Za-z0-9_]shorthand, no single-char character classes, bound your quantifiers, no two competing quantifiers over the same class, no\sunder MULTILINE, no.*?+ back-reference + DOTALL, anchored^/$, no leading^.*). Each rule cites the concrete review finding that produced it. Includes a ReDoS-exposure budget (10K-char pathological-input benchmark must stay ≤ 10ms) and test fixture hygiene rules (build credential-shaped strings from inert fragments at runtime). Linked fromcoding.md,code-review.md, thereview-prskill, andCLAUDE.md's "read before editing" entry point. code-review.mdchecklist gains a regex section — explicitgit diffrecipe to surface modifiedre.compile/re.match/re.subcalls + per-regex audit checklist.review-prskill gains a regex check — same checklist, applied during self-review before opening a PR.
Round-2.5 review surfaced two confirmed ReDoS shapes that the earlier rounds missed; the regex hygiene sweep above also caught a handful of style-only deviations across the codebase.
- ReDoS confirmed in
_MARKDOWN_HEADING_PATTERN(forgelm/ingestion.py) — Old pattern[ \t]+(.+?)[ \t]*$had three quantifiers competing for trailing whitespace; pathological input"# a" + " \t" * n + "x"ran in O(n²) time (100ms at n=2000, 600ms at n=5000, 2.1s at n=10000 measured in CPython 3.11). Replaced with a non-whitespace anchor on the body capture:[ \t]+(\S(?:[^\n]*\S)?)[ \t]*$. Result: linear (10μs at n=10000 — 200000× speedup). _CODE_FENCE_BLOCKregex replaced with state machine (forgelm/data_audit.py) — Old form used.*?+ back-reference +re.DOTALL, which SonarCloudpython:S5852flags as a polynomial-runtime risk even though it benchmarks linearly in CPython. Replaced with a per-line state machine (_strip_code_fences+_is_code_fence_open+_is_code_fence_close) that is provably O(n) and matches the same line-walker pattern as_markdown_sections. Behaviour pinned bit-for-bit on 7 fixtures.[A-Za-z0-9_-]→[\w-]inopenai_api_key,google_api_key,jwt(3 places) regexes per regex.md rule 1.\s*$→[ \t]*$in_PUNCT_END_PATTERN(callers pre-split into single lines, so the\snewline-overlap is dead weight) per regex.md rule 5.- Bounded
_HF_HUB_ID_RE(forgelm/wizard.py) —[A-Za-z0-9._-]+→[\w.-]{1,96}(HF Hub username + repo name max length) per regex.md rule 3 — defence-in-depth, no behaviour change for well-formed HF IDs.
- New
TestRegexLinearityclass intests/test_phase12_review_fixes.py— pinned 1-second wall-clock cap on N=10000 pathological inputs for both_MARKDOWN_HEADING_PATTERNand_strip_code_fences. A real ReDoS would blow far past the threshold; a slow CI host won't false-positive. - Empirical sweep over all 25 forgelm regexes confirmed linear scaling under 50K-character adversarial input. Slowest pattern (
openssh_private_key, full-block PEM) measures 0.5ms — ~10μs/KB. The sweep is reproducible via the snippet documented in regex.md.
Second-round review of the Phase 12 commit surfaced 22 findings spanning correctness, regex coverage, code-smell hygiene, type widening, and documentation parity. All addressed.
- Private-key blocks redacted in full (
forgelm/data_audit.py) — Oldopenssh_private_key/pgp_private_keyregexes only matched theBEGINheader line, somask_secretsleft the entire base64 body +ENDline in clear text. Now both patterns match the full PEM/PGP envelope (BEGIN through matching END inclusive) underre.DOTALL. The literal block markers are split acrossr"-----" + r"BEGIN " + r"..."concatenations to keep repo-wide secret scanners (gitleaks / trufflehog) silent. - Fenced code blocks recognise tildes too (
forgelm/data_audit.py+forgelm/ingestion.py) —_CODE_FENCE_BLOCK(audit's quality-filter strip) and_MARKDOWN_CODE_FENCE(ingest's markdown splitter) only matched triple-backtick fences; CommonMark §4.5 also allows~~~. Both now recognise either fence character with up to 3 leading spaces. The markdown splitter additionally tracks the opening fence character so a stray\``inside a~~~` block (or vice-versa) doesn't toggle state. - DOCX block order preserved (
forgelm/ingestion.py:_iter_docx_blocks) —_extract_docxpreviously appended every paragraph followed by every table, reordering content. New helper walksdoc.element.bodyin source order, dispatches on<w:p>vs.<w:tbl>, and renders each block in place. - Markdown overlap rejected explicitly —
_strategy_dispatchand_strategy_dispatch_tokensraiseValueErrorwhen--strategy markdownis combined with a non-zero overlap, rather than silently dropping it. To keep the CLI's historical default--overlap 200from spuriously tripping the validator on a--strategy markdowninvocation that didn't ask for overlap,--overlap's argparse default is nowNone;ingest_pathresolves that sentinel to200for the sliding strategy and0for paragraph / markdown. minhash_distinctcounts unique sketches (forgelm/data_audit.py) — Previously returned the count of non-empty rows, breaking parity withsimhash_distinct(which is unique fingerprints). Now hashes each MinHash viam.hashvalues.tobytes()and counts the distinct set, matching simhash semantics._row_quality_flagstypedOptional[str]— The function already acceptedNoneat runtime; the signature now reflects that and the test's# type: ignore[arg-type]suppression is gone.- Cognitive-complexity refactors —
_row_quality_flags(CCN 22 → ≤ 10 via per-check helpers_check_low_alpha_ratio/_check_low_punct_endings/_check_abnormal_mean_word_length/_check_short_paragraphs/_check_repeated_lines);find_near_duplicates_minhash(CCN 21 → ≤ 10 via_build_minhash_lsh+_emit_minhash_pair);audit_dataset(CCN 21 → ≤ 12 via_fold_outcome_into_summary+_build_quality_summary+_build_near_duplicate_summary). - Regex / lint code-smells —
[A-Za-z0-9_]→\win the GitHub PAT pattern;[ ]{0,3}→{0,3}(single-char class collapsed) in markdown patterns;\s→[ \t]in heading pattern (mitigates the polynomial-backtracking concern SonarCloud flagged); duplicate"chunk_tokens must be positive"/"max_chunk_size must be positive"literal strings extracted to module constants_CHUNK_TOKENS_POSITIVE_MSG/_CHUNK_SIZE_POSITIVE_MSG;_MARKDOWN_OVERLAP_UNSUPPORTED_MSGconstant for the new validator; comprehension["| " + " | ".join(c for c in row) + " |"]simplified to["| " + " | ".join(row) + " |"]. - Documentation parity —
docs/guides/data_audit.mdquality-filter bullet list and JSON example now includerepeated_linesand a note about code-fence stripping.docs/guides/ingestion-tr.mdmirrors the EN guide's chunking-strategies table (markdown row added) and gains a new "secrets/credential masking (Phase 12)" section.CHANGELOG's Phase 12 entry no longer overstates the[ingestion-secrets]extra: the regex set is the sole detection backend in v0.5.2, and thedetect-secretspackage is reserved for a follow-up release.READMEseparates "From PyPI" and "From a local clone" install blocks so copy-paste users don't hit-e .confusion. - Test fixtures fragmented — All hardcoded credential / JWT literals in
tests/test_data_audit_phase12.py,tests/test_ingestion_phase12.py, andtests/test_phase12_review_fixes.pynow built at runtime from inert string fragments (e.g."AKIA" + "IOSFODNN7" + "EXAMPLE"). The regex still has to match the canonical shape, but no full literal credential lives in the source tree — silences gitleaks / trufflehog scans of the repo without changing behaviour. - 5 new round-2 regression tests (
tests/test_phase12_review_fixes.py) —TestTildeFenceRecognised(~~~-fenced code blocks block heading splits),TestPrivateKeyFullBlock(full PEM body redaction),TestMarkdownOverlapValidation(rejection on explicit non-zero overlap; default-overlap pass-through),TestMinHashDistinctSemantic(unique-sketches semantic). - Notebook ruff format —
notebooks/post_training_workflow.ipynbreformatted to satisfyruff format --checkin CI;notebooks/data_curation.ipynbinstall line pinned toforgelm[ingestion]==0.5.2rather than the movingmainbranch.
Round-1 review of the Phase 12 commit surfaced four critical regressions / bugs and several lower-severity issues. All addressed before tagging v0.5.2. No new functionality; only correctness, honesty, and parity fixes.
- JSON envelope back-compat (
forgelm/cli.py) —_run_data_audit's stdout JSON envelope dropped the v0.5.1near_duplicate_pairs_per_splittop-level key when the richernear_duplicate_summaryblock was added. Pre-Phase-12 CI consumers (jq '.near_duplicate_pairs_per_split.train') would have started gettingnull. Restored as an additive key alongside the new one. Plan / CHANGELOG language updated from "byte-identical default report" to "schema-additive" — older parsers keep working, but on-disk JSON is no longer byte-identical becausesecrets_summary,near_duplicate_summary.method, andcross_split_overlap.methodare now always present. - Quality filter completes the planned check set (
forgelm/data_audit.py) — Plan promised five Gopher / C4 / RefinedWeb-style heuristics; v0.5.2 shipped four. Added the missingrepeated_linescheck (top-3 actually-repeating distinct lines covering > 30 % of non-empty lines flag the row — pinned on count ≥ 2 so short all-unique documents don't false-positive). Surfaces inquality_summary.by_check.repeated_lines. - Quality filter respects fenced markdown code (
forgelm/data_audit.py:_strip_code_fences) — Code blocks legitimately have low alpha ratio + missing end-of-line punctuation + short paragraphs and tripped every prose heuristic, polluting thequality_summaryon legitimate code-instruct corpora._row_quality_flagsnow strips fenced…blocks before applying the heuristics; pure-code rows return[]instead of being flagged on shape grounds. - DOCX table cells escape
|and\(forgelm/ingestion.py:_escape_md_cell) —_docx_table_to_markdownjoined cell text directly into a markdown table row, so a cell containinga|bwas parsed by downstream tokenisers as two extra columns. Now escapes|→\|and\→\\per CommonMark, and collapses embedded newlines to spaces (markdown tables can't carry multi-line cells). - JWT regex narrowed (
forgelm/data_audit.py) — Old pattern\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\bfalse-positived on prose likeeyJfoo.eyJbar.baz. Anchored on the canonical JWT header alphabet (alg/typ/kid/cty/enc/api's base64url prefixes —hbGc,0eXA,raWQ,jdHk,lbmM,hcGk) plus minimum lengths on payload and signature. Real JWTs (including the original test fixture) still match; arbitraryeyJ.eyJ.X-shaped prose does not. - MinHash docstring honest about the metric (
forgelm/data_audit.py) —compute_minhashpreviously claimed it surfaces "the same class of near-duplicates" as simhash. The two use different similarity metrics (set-Jaccard over distinct tokens vs. frequency-weighted bit-cosine) and disagree on documents with high token-frequency variance. Docstring rewritten to make the divergence explicit. Roadmap "byte-identical" wording corrected in the same spirit. - CommonMark indented headings recognised (
forgelm/ingestion.py) —_MARKDOWN_HEADING_PATTERNand_MARKDOWN_CODE_FENCEallow up to 3 leading spaces per CommonMark §4.2; 4+ spaces still fall through as indented code blocks (correctly not split as headings). - Cog complexity restored to ≤ 15 —
_aggregator_to_info(split into_populate_schema_block/_populate_optional_findings/_within_split_pairs) and_markdown_sections(split into_push_heading_onto_path/_trim_blank_edges) factored to stay under the Phase 11.5 ceiling. - Defensive lazy import in
compute_minhash— Empty input now returnsNonewithout paying the_require_datasketch()raise path. Same effect for_count_leaked_rows_minhashwhen the entire target list is empty (LSH-construction skipped). - Mask-order rationale honest (
forgelm/ingestion.py:_emit_chunk) — Old docstring claimed today's regex sets overlap; in practice the shipped fixtures show no overlap. Rewritten to describe the ordering as defensive (favour secrets when ordering matters at all; future-proof against new PII / secret regexes that may overlap, e.g. Azure connection strings vs. IBANs). - Markdown chunkers document the no-overlap contract —
_chunk_markdownand_chunk_markdown_tokensdocstrings explicitly state that--overlap/--overlap-tokensare silently ignored when--strategy markdownis selected (sections are atomic; overlapping would slice mid-section and break the breadcrumb invariant). - Type hints tightened —
IngestionResult.format_counts/pii_redaction_counts/secrets_redaction_countsand the local counters iningest_pathtyped asDict[str, int]instead of baredict. - Turkish documentation parity (
docs/guides/data_audit-tr.md) — Three Phase 12 H3 sections (MinHash LSH, Code/secret tagger, Heuristic quality filter) were missing from the TR mirror; added at the same detail level as the EN guide. - 18 regression tests (
tests/test_phase12_review_fixes.py) — One class per finding, pinning the fixes against re-introduction. Covers the JSON envelope shape,repeated_linesdetection on real boilerplate vs. all-unique short docs, DOCX|/\/ newline escaping, JWT header-alphabet anchors with the prose-shape false-positive, code-fence stripping in the quality filter, the token-aware markdown chunker (previously untested), and CommonMark 0-3-space indented headings.
Direct continuation of the Phase 11 / 11.5 ingestion + audit lineage. Closes the four concrete gaps surfaced by the post-v0.5.1 competitive review (LLaMA-Factory / Axolotl / Unsloth / NeMo Curator / Dolma / RedPajama / LlamaIndex / LangChain / Marker / Docling). Tier 1 (5 must-have tasks) shipped; Tier 2/3 (Presidio adapter, Croissant metadata, --all-mask, wizard "audit first") deferred to a Phase 12.5 backlog.
- MinHash LSH dedup option (
forgelm/data_audit.py:compute_minhash,find_near_duplicates_minhash,_count_leaked_rows_minhash) — Opt-in--dedup-method minhash --jaccard-threshold 0.85route via the optional[ingestion-scale]extra (datasketch>=1.6.0). Default simhash + LSH banding from Phase 11.5 stays untouched and remains the only method that runs without an optional dependency.audit_dataset(...)API gainsdedup_method,minhash_jaccard,minhash_num_permparameters;near_duplicate_summary.methodrecords which path ran. Cross-split overlap + within-split duplicate scan share the same method flag. MinHash is approximate (permutation noise; defaultnum_perm=128) — pinnum_permfor cross-run determinism. - Markdown-aware splitter (
forgelm/ingestion.py:_chunk_markdown,_chunk_markdown_tokens,_markdown_sections,_heading_breadcrumb) — New--strategy markdownparses heading hierarchy (# H1…###### H6), keeps code-fenced blocks atomic (heading-shaped lines inside```blocks are not interpreted as section boundaries), and inlines a heading breadcrumb at the top of each chunk so SFT loss sees the document context. Composes with the Phase 11.5 token-aware mode (--chunk-tokens+--tokenizer). - Code / secret leakage tagger (
forgelm/data_audit.py:detect_secrets,mask_secrets,_SECRET_PATTERNS) — Always-on audit-side scan with a prefix-anchored regex set (the sole detection backend in this release) covering AWS access keys (AKIA…/ASIA…), GitHub PATs (ghp_,gho_,ghs_,ghu_,ghr_,github_pat_), Slack tokens, OpenAI API keys (sk-…/sk-proj-…), Google API keys, JWTs anchored on canonical header alphabet, full OpenSSH / RSA / DSA / EC / PGP private-key blocks (BEGIN through END inclusive —mask_secretsredacts the entire block, not just the header line), and Azure storage connection strings. Adds asecrets_summaryblock alongsidepii_summary. Ingest side:forgelm ingest --secrets-maskrewrites detected spans with[REDACTED-SECRET]; runs before PII masking as a defensive ordering so future overlapping detectors (PII vs secret regex) can't double-count. The optional[ingestion-secrets]extra (detect-secrets>=1.5.0) is reserved for a follow-up release — the current code does not invoke thedetect-secretspackage (its plugin model assumes file paths, not streaming chunks); install only as forward-compatibility for the eventual integration. - Heuristic quality filter (
forgelm/data_audit.py:_row_quality_flags,_QUALITY_CHECKS) — Opt-inforgelm audit --quality-filterruns Gopher / C4 / RefinedWeb-style checks per row:low_alpha_ratio(< 70 % letters among non-whitespace),low_punct_endings(< 50 % of non-empty lines end with punctuation),abnormal_mean_word_length(outside 3–12 chars),short_paragraphs(> 50 % of\n\n-blocks have < 5 words). Surfacesquality_summarywith per-check counts,samples_flagged, andoverall_quality_score. ML-based classifiers (fastText / DeBERTa) deliberately out of scope — keeps the audit deterministic for Annex IV reproducibility. - DOCX / Markdown table preservation (
forgelm/ingestion.py:_docx_table_to_markdown) —_extract_docxnow renders tables as markdown table syntax (header row +---separator + body rows) instead of the previous" | "-joined flat line. Uneven rows are right-padded with empty cells; all-blank rows are dropped; the first non-empty row becomes the header (no heuristic — that's the convention DOCX authors use). Combined with--strategy markdownthe table block stays intact across chunks.
AuditReportgainssecrets_summary: Dict[str, int]andquality_summary: Dict[str, Any]fields (additive — Phase 11/11.5 consumers reading justpii_summary/near_duplicate_summarykeep working).IngestionResultgainssecrets_redaction_counts: dictfield.audit_dataset(...)acceptsdedup_method,minhash_jaccard,minhash_num_perm,enable_quality_filterkeyword arguments.ingest_path(...)acceptssecrets_mask: boolkeyword argument.- New constants:
DEDUP_METHODS,DEFAULT_MINHASH_JACCARD,DEFAULT_MINHASH_NUM_PERM,SECRET_TYPES.
forgelm ingest:--strategy markdown,--secrets-mask.forgelm audit:--dedup-method {simhash,minhash},--jaccard-threshold X(validated to[0.0, 1.0]at parse time),--quality-filter.- New argparse type helper
_non_negative_float(mirrors_non_negative_int's pattern). _run_data_auditnow distinguishesEXIT_CONFIG_ERROR(filesystem/path errors) fromEXIT_TRAINING_ERROR(missing[ingestion-scale]extra when--dedup-method=minhashwas requested).
- New extras:
[ingestion-scale](datasketch>=1.6.0,<2.0.0),[ingestion-secrets](detect-secrets>=1.5.0,<2.0.0). - Version bump
0.5.1rc1 → 0.5.2rc1.
tests/test_data_audit_phase12.py— 18 new tests acrossTestSecretsDetection,TestSecretsMasking,TestAuditPicksUpSecrets,TestQualityFilterPerRow,TestQualityFilterEnabled,TestMinHashLshDedup(skipped withoutdatasketch),TestMinHashMissingExtra.tests/test_ingestion_phase12.py— 13 new tests acrossTestMarkdownSections,TestChunkMarkdown,TestMarkdownStrategyExposed,TestDocxTableToMarkdown,TestSecretsMaskIngest.tests/test_cli_subcommands.py—test_audit_quality_filter_flag,test_audit_rejects_invalid_jaccard_thresholdadded toTestAuditSubcommand.
_StreamingAggregatorgainsminhashes,secrets_counts,quality_flags_counts,quality_samples_flagged,dedup_method,minhash_num_perm,enable_quality_filterfields. Field rename:_SplitOutcome.fingerprints→_SplitOutcome.signatures(the same field carries simhash ints OR MinHash instances, depending on method)._audit_split(...)now returns(info, signatures, pii_split, parse_errors, decode_errors)wheresignaturesis method-dependent._process_splitandaudit_datasetwere updated in lockstep._pair_leak_payloadand_cross_split_overlapswitched to keyword-onlydedup_methodparameter and dispatch on it (simhash → Hamming; minhash → Jaccard).describe_strategies()now listsmarkdownalongsidesliding/paragraph/semantic.
Operational polish on top of v0.5.0's ingestion + audit surface — no new training capabilities, but materially better handling for large corpora and a cleaner CLI shape. All 12 follow-ups carved out of Phase 11's review backlog.
forgelm audit PATHsubcommand — Promotes the--data-audittop-level flag to a first-class subcommand with--verbose,--near-dup-threshold, and its own--outputdefault (./audit/). The legacyforgelm --data-audit PATHflag keeps working as a deprecation alias and logs a one-line notice; existing CI pipelines need no changes. Removal targeted no earlier thanv0.7.0.- LSH-banded near-duplicate detection (
find_near_duplicates,_count_leaked_rows) — Pigeonhole-banded LSH (defaultbands = threshold + 1) drops within-split + cross-split scans fromO(n²)to ~O(n × k). Recall stays exact at the default Hamming threshold; brute-force fallback remains for edge thresholds where bands shrink below 4 bits. Unblocks audits on 100K+ row corpora. - Streaming
_read_jsonl_split— The audit's JSONL reader is now a generator yielding(row, parse_err, decode_err);_audit_splitconsumes it row-by-row via a_StreamingAggregatorso RAM stays bounded on multi-million-row splits. Per-line tolerance semantics (parse errors, decode errors, non-dict rows) preserved. - Token-aware ingestion (
--chunk-tokens,--tokenizer,--overlap-tokens) — Optional flags onforgelm ingestsize chunks against an HFAutoTokenizer.encodeinstead of raw character counts, so chunks line up withmodel.max_lengthexactly.--tokenizeris required with--chunk-tokens(we refuse to default to a hidden vocab because the chunk count would silently differ per-model).trust_remote_code=Falseis hard-pinned for safety. - PDF page-level header / footer dedup (
_strip_repeating_page_lines) — Lines that recur as the first or last non-empty line on ≥ 70 % of a PDF's pages (company watermarks, page numbers, copyright lines) are stripped automatically before chunking. Reduces auditnear_duplicate_pairsnoise on long policy / book PDFs. Skipped on documents shorter than 3 pages. - PII severity tiers — Audit JSON now carries a
pii_severityblock (total,by_tier,by_type,worst_tier) alongside the flatpii_summary. Tiers map regulatory weighting:credit_card/iban→ critical (PCI-DSS), national IDs → high (GDPR Art. 9),email→ medium,phone→ low. The aggregate notes line leads with the worst tier (WORST tier: CRITICAL) so reviewers cannot miss it. summarize_reporttruncation policy — Defaultverbose=Falsefolds zero-finding splits into a single tail line so multi-split summaries stay short;--verboseon the newauditsubcommand reverses this for full output. Has no effect on the on-disk JSON report.- Structured ingestion notes —
IngestionResult.extra_noteskeeps the human-readable list; newnotes_structured: {key: value}(and an explicitpdf_header_footer_lines_strippedfield) carries machine-readable counts for CI/CD consumers. JSON output exposes both. - Wizard "ingest first" entry point —
_offer_ingest_for_directory+_prompt_dataset_path_with_ingest_offer: BYOD quickstart and the full 8-step wizard now offer to runforgelm ingestinline when the typed dataset path is a directory of raw documents, then feed the produced JSONL straight back into the BYOD path. Closes the onboarding loop end-to-end. - xxhash backend for simhash + token-level memo — Optional
xxhash.xxh3_64digest path (added toforgelm[ingestion]); BLAKE2b stays as the fallback. The Python-level speedup is modest (~1.3× raw, ~1.05× end-to-end after the cache below absorbs Zipfian repeats — xxhash's "4-10×" figure refers to C-level pure-hash microbenchmarks, not the Python wrapping path). The bigger wall-clock win is the new module-scopelru_cache(maxsize=10_000)that memoises the per-token digest — most corpora's token traffic is dominated by a few thousand frequent tokens, so the cache hit rate is very high. - Atomic audit-report write —
data_audit_report.jsonis now written viatempfile.NamedTemporaryFile+os.replaceso a crashed audit can never leave a half-written report on disk.newline="\n"pinned for byte-exact reproducibility across Windows / Linux / macOS.
tests/test_data_audit.py—TestLshBandedNearDuplicates(LSH parity vs. brute force + high-threshold fallback),TestPiiSeverity(critical-tier verdict + neutral case),TestSummarizeVerbosePolicy(clean splits folded vs. expanded),TestAtomicWrite(no.tmpleftovers),TestStreamingReader(per-line tuple yields),TestTokenCachePerformance(cross-text cache hits).tests/test_ingestion.py—TestPdfHeaderFooterDedup(multi-page header/footer collapse, short-doc skip, no-repeats pass-through),TestStructuredIngestionNotes,TestTokenAwareCli(validates the--chunk-tokensrequires--tokenizerrule).tests/test_cli_subcommands.py—TestAuditSubcommand(subcommand happy path, JSON envelope, legacy--data-auditalias).tests/test_wizard_byod.py— refreshed for the new ingest-first wording (empty directory rejection, decline-the-ingest-offer path).
AuditReportgains apii_severity: Dict[str, Any]field. JSON consumers reading onlypii_summarycontinue to work; the new field is additive.find_near_duplicates(fingerprints, *, threshold, bits=64)accepts abitskeyword for adaptive banding (default 64 matchescompute_simhash)._read_jsonl_splitis now a generator. The legacy buffered tuple return is gone — callers that were materialising rows can wrap withlist(...)._audit_split(split_name, path, ...)now takes a path instead of an in-memory list;_process_splitcalls it directly. Returns(info, fingerprints, pii_split, parse_errors, decode_errors)so OSError handling stays in the orchestrator.
Document Ingestion & Data Audit (Phase 11) — bridges raw enterprise corpora (legal, medical, policy manuals) to ForgeLM's training data format and surfaces governance signals before training starts.
-
forgelm/ingestion.py+forgelm ingestsubcommand:- Multi-format extractors: PDF (
pypdf), DOCX (python-docx), EPUB (ebooklib+beautifulsoup4), TXT, Markdown. - Two chunking strategies:
paragraph(default; greedy, never splits a paragraph) andsliding(fixed window with--overlap).semanticraisesNotImplementedErrorand is reserved for a follow-up phase. - Output is
{"text": "..."}JSONL — recognized as pre-formatted SFT input byforgelm/data.pywithout further preprocessing. --recursivewalks directory trees; unsupported extensions are skipped silently, supported files with no extractable text skip with a warning.--pii-maskredacts detected PII spans before chunks land in the JSONL (shared regex set with the audit module).- OCR is intentionally out of scope; scanned PDFs without a text layer warn and produce zero chunks.
- Multi-format extractors: PDF (
-
forgelm/data_audit.py+forgelm --data-audittop-level flag:- Per-split metrics: sample count, column schema, text length distribution (
min/max/mean/p50/p95), null/empty rate, top-3 language detection (best-effort vialangdetect). - 64-bit simhash near-duplicate detection within each split; Hamming-distance threshold 3 ≈ 95% similarity (canonical web-page-dedup setting).
- Cross-split overlap report — guards against silent train-test leakage that destroys benchmark fidelity.
- PII regex set (
email,phone,credit_cardLuhn-validated,iban,tr_idchecksum-validated,de_id,fr_ssn,us_ssn); per-split + aggregate counts. - Layout: single
.jsonlfile → treated astrain; directory containingtrain.jsonl/validation.jsonl/test.jsonl(any subset) auto-discovered. - Writes
data_audit_report.jsonunder--output(default./audit/);--output-format jsonmirrors the report on stdout for CI/CD pipelines. - CPU-only; no GPU, no network.
- Per-split metrics: sample count, column schema, text length distribution (
-
EU AI Act Article 10 integration —
generate_data_governance_reportnow inlinesdata_audit_report.jsonunder thedata_auditkey when present in the trainer'soutput_dir. Compliance bundle becomes a single self-contained document instead of a pointer. -
pyproject.toml[ingestion]extra —pypdf,python-docx,ebooklib,beautifulsoup4,langdetect. Cross-platform, no native compilation. -
Tests —
tests/test_ingestion.py(TXT path + chunking strategies; PDF round-trip skips whenpypdfmissing) andtests/test_data_audit.py(PII regex + Luhn / TC Kimlik validators, simhash properties, end-to-end audit on file + split-keyed directory layouts, governance integration). All GPU/network-free. -
Documentation — new guides at
docs/guides/ingestion.mdanddocs/guides/data_audit.md; README feature section, CLI epilog, install matrix, and roadmap status updated.
Quickstart Layer (Phase 10.5) — One-command bundled templates with opinionated defaults. Primary community-growth driver: closes the gap between "I just installed ForgeLM" and "I have a fine-tuned model running locally."
-
forgelm/quickstart.py— Template registry + orchestrator:Template(frozen dataclass) —name,title,description,primary_model,fallback_model,trainer_type,estimated_minutes,min_vram_for_primary_gb,bundled_dataset,license_note.TEMPLATES: Dict[str, Template]— 5 entries:customer-support,code-assistant,domain-expert,medical-qa-tr,grpo-math.auto_select_model(template, available_vram_gb)— picks primary model when VRAM ≥ threshold (10–12 GB), fallback otherwise; explicitno-gpu-detectedreason when CUDA is absent._detect_available_vram_gb()— wrapstorch.cuda.mem_get_info(); returnsNonewhen no GPU (test mock point).run_quickstart(template_name, *, model_override, dataset_override, output_path, dry_run, available_vram_gb)→QuickstartResult— copies seed dataset, substitutesmodel.name_or_pathanddata.dataset_name_or_path, writesconfigs/<template>-YYYYMMDDHHMMSS.yaml. Generated YAML is identical in shape to a hand-written one — same trainer, same schema.format_template_list(),summarize_result(result)— text/JSON renderers for CLI use.
-
forgelm quickstart <template>CLI subcommand (inforgelm/cli.py):--list— prints the registry; honors top-level--output-format jsonfor CI.--model <id>— override auto-selected model.--dataset <path>— override the bundled seed dataset (required fordomain-expert).--output <path>— custom YAML output path (default:./configs/<template>-<timestamp>.yaml).--dry-run— generate config only; skip training and chat.--no-chat— train but skip the post-training chat REPL.- On a successful run, subprocess-invokes
forgelm --config <out>and thenforgelm chat <output_dir>(unless--no-chat).
-
Wizard integration —
forgelm --wizardnow opens with "Start from a template?":- Yes → routes to the quickstart selector; the wizard becomes a thin shell over
run_quickstart(). - No → falls through to the existing 8-step interactive flow.
- No bifurcation: identical code paths and YAML schema downstream.
- Yes → routes to the quickstart selector; the wizard becomes a thin shell over
-
5 bundled templates under
forgelm/templates/:customer-support/— Qwen2.5-7B-Instruct primary, SmolLM2-1.7B-Instruct fallback. SFT trainer. 58-example seed JSONL in{"messages": [...]}format.code-assistant/— Qwen2.5-Coder-7B-Instruct primary, Qwen2.5-Coder-1.5B-Instruct fallback (code-tuned smaller variant, not generic SmolLM2). SFT. 59-example Python/programming Q&A.domain-expert/— Qwen2.5-7B-Instruct primary, SmolLM2-1.7B-Instruct fallback. BYOD; empty data with a README explaining how to pair withforgelm ingest(Phase 11) or a custom JSONL.medical-qa-tr/— Qwen2.5-7B-Instruct primary, Qwen2.5-1.5B-Instruct fallback (Turkish-capable, not English-only SmolLM2). SFT, 49 Turkish Q&A; every answer ends with "Tıbbi acil durumlarda 112'yi arayın..." (medical-disclaimer guardrail).grpo-math/— Qwen2.5-Math-7B-Instruct primary, Qwen2.5-Math-1.5B-Instruct fallback. GRPO trainer (grpo_num_generations: 4). 40 grade-school math word problems in prompt-only format, each carrying agold_answerfield for the built-in regex correctness reward.
-
Conservative defaults in every template config:
- QLoRA 4-bit NF4, LoRA rank=8,
per_device_train_batch_size=1, gradient checkpointing on, safety eval / compliance artifacts opt-in only. - Designed so the smallest fallback model + the bundled seed dataset run end-to-end on a 12 GB consumer GPU.
- QLoRA 4-bit NF4, LoRA rank=8,
-
forgelm/templates/LICENSES.md— Full attribution for bundled seed datasets (CC-BY-SA 4.0, author-original); contributing guide for new templates; medical-disclaimer note formedical-qa-tr. -
pyproject.toml[tool.setuptools.package-data]— bundles*.yaml,*.jsonl,*.mdunderforgelm.templatesinto the wheel sopip install forgelmusers get the templates without a source checkout. -
GRPO baseline reward —
forgelm/grpo_rewards.pyships a default reward bundle so prompt-only datasets don't crash insidetrl.GRPOTrainer. Whengrpo_reward_modelis unset the trainer wirescombined_format_length_reward(0.8 × format-match + 0.2 × length-shaping); if the dataset additionally carries agold_answerfield (the bundledgrpo-mathseed does),_math_reward_fnis appended so TRL sums correctness on top of format teaching. -
Tests — All GPU-independent via TRL/torch FSDP-aware skip-if pattern:
tests/test_quickstart.py— registry consistency, bundled-asset shape,auto_select_modelprimary/fallback/no-gpu, end-to-endrun_quickstart, CLI dispatch, regression test that loads every generated YAML throughload_config(strongest guard against template drift).tests/test_quickstart_hardening.py— PR review hardening (path validation, model override edges, dry-run wiring).tests/test_grpo_math_reward.py— pure-Python unit tests for_normalize_answer,_answers_match,_math_reward_fn,_dataset_has_gold_answers.tests/test_grpo_format_reward.py—format_match_reward,length_shaping_reward,combined_format_length_reward, plus trainer integration.tests/test_wizard_byod.py— wizard BYOD dataset path validation (existence, directory, malformed JSONL, valid JSONL, HF Hub IDs,~expansion).tests/test_cli_quickstart_wiring.py—--offlinepropagation, separate chat inheritance, chat exit-code 0/130 handling.tests/test_packaging.py— wheelpackage_datasmoke (catches editable-install-only template paths).tests/test_grpo_reward.py— extended with no-reward-model + gold-answer wiring assertions.
-
CI —
.github/workflows/nightly.yml:- Per-template quickstart smoke (4 of 5 —
domain-expertis BYOD and covered by pytest). - New
wheel-install-smokejob: builds the wheel, installs it into a fresh venv from/tmp(so the source tree is offsys.path), and rerunsquickstart --list+quickstart --dry-runto catch brokenpackage_dataglobs that editable installs hide.
- Per-template quickstart smoke (4 of 5 —
- New "Option 0: One-Command Quickstart Template" section at the top of
docs/guides/quickstart.md. docs/roadmap.md,docs/roadmap-tr.md,docs/roadmap/completed-phases.md,docs/roadmap/releases.mdupdated to mark Phase 10.5 as Done.README.mdquickstart section updated to lead withforgelm quickstart.
Post-Training Completion (Phase 10)
-
forgelm/inference.py— Shared generation primitives for all post-training features:load_model(path, adapter, backend, load_in_4bit, load_in_8bit, trust_remote_code)— loads HF model + tokenizer; optional PEFT adapter merge viamerge_and_unload(); unsloth backend supportgenerate(model, tokenizer, prompt, *, messages, system_prompt, history, max_new_tokens, temperature, top_k, top_p, repetition_penalty)— non-streaming text generationgenerate_stream(...)— streaming viaTextIteratorStreamerin daemon thread; yields token chunkslogit_stats(logits)— returns{entropy, top1_prob, effective_vocab}for token-level confidence inspectionadaptive_sample(logits, temperature, top_k, top_p, entropy_threshold)— greedy below entropy threshold, nucleus sampling above_build_prompt— usestokenizer.apply_chat_templatewhen available; falls back to"role: content\n"join
-
forgelm/chat.py— Interactive terminal REPL (ChatSessionclass +run_chat()entry point):- Streaming output by default;
--no-streamflag for non-streaming - Slash commands:
/reset,/save [file],/temperature N,/system [prompt],/help,/exit - History management with 50-turn cap (
_MAX_HISTORY_PAIRS) - Optional
richrendering viapip install forgelm[chat] - Optional
--safetyflag routes each response through Llama Guard
- Streaming output by default;
-
forgelm/fit_check.py— VRAM pre-flight advisor:estimate_vram(config)→FitCheckResult(verdict, estimated_gb, available_gb, breakdown, recommendations)- Verdicts:
FITS(< 85% GPU),TIGHT(85-95%),OOM(> 95%),UNKNOWN(no GPU) - Architecture loaded via
transformers.AutoConfig; fallback size-hint dict for 7b/8b/13b/70b families - VRAM components: base weights + LoRA adapter + optimizer state (AdamW/8-bit/GaLore-aware) + activations (gradient-checkpointing divides by √layers)
format_fit_check(result)— human-readable summary;--output-format jsonfor CI/CD- Hypothetical mode when no CUDA detected — still estimates based on architecture
-
forgelm/export.py— GGUF model export:export_model(model_path, output_path, *, format, quant, adapter, update_integrity, extra_args)→ExportResult- Wraps
llama-cpp-python'sconvert_hf_to_gguf.py— no reimplementation of conversion logic - Supported quantizations:
q2_k,q3_k_m,q4_k_m,q5_k_m,q8_0,f16 - K-quant note:
q2_k/q3_k_m/q4_k_m/q5_k_mrequire a two-step flow.forgelm export ... --quant q4_k_m model.ggufproduces an intermediatemodel.f16.gguf; runllama-quantize model.f16.gguf model.gguf Q4_K_Mafterward to obtain the K-quant. TheExportResult.quantfield reflects what was actually written (somodel_integrity.jsonSHA-256 stays honest) - Adapter merge: loads base + PEFT, saves merged fp16 weights before conversion
_sha256_file— chunked 64 KB reads for large models_update_integrity_manifest— appends export artifact (path, quant, sha256, size_bytes) tomodel_integrity.json- Optional dependency:
pip install forgelm[export](llama-cpp-python>=0.2.90)
-
forgelm/deploy.py— Deployment config file generation:generate_deploy_config(model_path, target, output_path, *, system_prompt, max_length, temperature, top_k, top_p, ...)→DeployResult- Target
ollama: Modelfile with FROM, SYSTEM (double-quote escaped), PARAMETER directives - Target
vllm: YAML engine config with GPU memory utilization, dtype, trust_remote_code - Target
tgi: docker-compose.yaml with GPU resource reservation, port mapping, max-input/total-length - Target
hf-endpoints: JSON spec with model repository, task, compute instance, region, framework - Case-insensitive target matching; default output filenames per target
-
CLI subcommands (
forgelm/cli.py):forgelm chat MODEL_PATH [--adapter] [--system] [--temperature] [--max-new-tokens] [--safety] [--no-stream] [--load-in-4bit] [--load-in-8bit] [--trust-remote-code] [--backend]forgelm export MODEL_PATH --output FILE [--format gguf] [--quant q4_k_m] [--adapter] [--no-integrity-update]forgelm deploy MODEL_PATH --target TARGET [--output FILE] [--system] [--max-length] [--temperature] [--top-k] [--top-p] [--trust-remote-code]forgelm --config CONFIG --fit-check [--output-format json]- All subcommands work without
--config; backward-compatible with existing flat CLI
-
Optional extras in
pyproject.toml:forgelm[export]—llama-cpp-python>=0.2.90(non-Windows)forgelm[chat]—rich>=13.0.0
-
New test modules:
tests/test_inference.py— 16 tests covering_build_prompt,_to_messages,logit_stats,adaptive_sample,load_model,generatewith custom torch stub (no GPU required)tests/test_fit_check.py— 18 tests covering parameter estimation, VRAM components, GPU scenarios (no CUDA, 4 GB, 80 GB),format_fit_checktests/test_export.py— 12 tests covering SHA-256, integrity manifest, GGUF export flow with subprocess mocktests/test_deploy.py— 21 tests covering all 4 target generators andgenerate_deploy_configintegrationtests/test_cli_phase10.py— 22 tests covering--fit-check, all deploy targets, export subcommand, chat subcommand, subcommand routing
forgelm/__init__.py— version bumped to0.4.0forgelm/cli.py— added subparser architecture withchat,export,deploysubcommands; added--fit-checkflag;KeyboardInterruptcaught in chat dispatch for graceful exitforgelm/wizard.py— (no changes needed; Phase 10 features are all CLI-driven, not wizard-driven)
forgelm.compliance.export_compliance_artifactssignature changed from(manifest, config, output_dir)to(manifest, output_dir). Theconfigargument was unused (the manifest already contains all derived values). External callers must drop the second positional argument.forgelm.export.export_modelkeywordformat=renamed tooutput_format=to avoid shadowing theformatbuiltin. Updateexport_model(..., format="gguf", ...)→export_model(..., output_format="gguf", ...).forgelm.deploy.generate_deploy_configparameter list collapsed from 18 → 11 args. The HF Endpoints fields (task/instance_size/instance_type/ region/framework/vendor) are now grouped ashf_endpoints: HFEndpointsOptions = None; sampling defaults (temperature/top_k/top_p) are grouped assampling: SamplingOptions = None. Pass instances of those dataclasses instead of the individual kwargs.
- Engineering standards (
docs/standards/) — 9 standard documents: coding, architecture, error-handling, logging-observability, testing, documentation, localization, code-review, release. - AI agent skills (
.claude/skills/) — 6 task-specific SKILL.md checklists: add-config-field, add-trainer-feature, add-test, sync-bilingual-docs, cut-release, review-pr. - CLAUDE.md — Root-level AI agent guidance file with non-negotiable project principles, skill table, and repo structure map.
- Phase 10-13 planning docs (
docs/roadmap/phase-*.md) — Detailed planning for Post-Training Completion, Data Ingestion, Quickstart Layer, and Pro CLI.
- docs/ reorganization — Reference docs moved to
docs/reference/, design specs todocs/design/. All internal links updated (29 link fixes). - Roadmap refactored —
docs/roadmap.mdreduced from 910 to 78 lines; phase details moved todocs/roadmap/subdirectory.
- Webhook URLs excluded from HuggingFace Hub model cards — prevents credential leaks
- User-supplied strings sanitized before Markdown template embedding (content injection prevention)
- All 19 Pydantic sub-models enforce
extra="forbid"— YAML typos are errors, not silent bugs - Deprecated
lora.use_dora/lora.use_rslorabooleans auto-normalize tolora.methodwith warnings - Audit log hash chain restores continuity across process restarts
- Compliance manifests correctly report pre-OOM-recovery batch size
- GRPO reward model path correctly wrapped as callable
- Safety classifier receives full
[INST] prompt [/INST] responsecontext - Extension-less files raise clear
ValueErrorinstead of silently loading wrong format - TIES tie-breaking fixed; DARE now deterministic with
seed=42
GaLore Optimizer Integration
- Full-parameter training via gradient low-rank projection — alternative to LoRA
- 6 optimizer variants:
galore_adamw,galore_adamw_8bit,galore_adafactor, + layerwise versions - Configurable rank, update_proj_gap, scale, proj_type, target_modules
- Validation: layerwise + multi-GPU incompatibility detection, LoRA co-existence warning
Long-Context Optimizations
- RoPE scaling support: linear, dynamic, YaRN, LongRoPE with configurable factor
- NEFTune noise injection (
neftune_noise_alpha) for improved training quality - Sliding window attention override for Mistral-family models
- Sample packing for efficient short-sequence training
Synthetic Data Pipeline
- Teacher→student distillation with
--generate-dataCLI command - Three teacher backends: API (OpenAI-compatible), local (HuggingFace model), file (pre-generated)
- Configurable system prompt, temperature, max_new_tokens, rate limiting
- Four output formats: messages (chat), instruction, chatml, prompt_response
- Seed prompts from JSONL file or inline config
GPU Cost Estimation
- Auto-detection for 18 GPU models (T4, A100, H100, RTX 4090, etc.)
- Per-run cost calculation based on training duration and GPU type
- Manual override via
training.gpu_cost_per_hour
CI/CD & Publishing
- PyPI publishing workflow (
.github/workflows/publish.yml) —pip install forgelm - Nightly compatibility testing (
.github/workflows/nightly.yml) - Expanded adversarial prompt library: 140 prompts across 6 categories (was 50/3)
Wizard Enhancements
- GaLore strategy option with rank and optimizer selection
- Long-context auto-detection (max_length > 4096) with RoPE scaling prompt
- NEFTune noise injection option
- SFTConfig
max_length→max_seq_lengthfor TRL compatibility device_map={"":0}for single GPU without 4-bit (prevents model splitting)- Gradient checkpointing disabled on CPU (requires CUDA)
- Pre-formatted
textcolumn datasets now properly handled - Chat template applied during inference in notebooks
- Version bump: 0.2.0 → 0.3.0
- All notebooks use SmolLM2-135M for faster Colab testing (was 1.7B)
- Notebooks include base vs fine-tuned model comparison
- 297 tests (up from 242), 0 lint errors
Major release: ForgeLM goes from a basic SFT fine-tuning tool to a full-stack LLM training platform with alignment, distributed training, safety evaluation, and EU AI Act compliance.
Alignment & Post-Training Stack
- 6 trainer types: SFT, DPO, SimPO, KTO, ORPO, GRPO
- Per-trainer hyperparameters (
dpo_beta,kto_beta,grpo_num_generations, etc.) - Dataset format auto-detection with trainer_type mismatch suggestions
Distributed Training
- DeepSpeed ZeRO-2, ZeRO-3, ZeRO-3+Offload presets
- FSDP support with sharding strategies (FULL_SHARD, SHARD_GRAD_OP)
- Unsloth + distributed conflict detection
Safety & Evaluation
- Safety classifier gate (Llama Guard) with binary and confidence-weighted scoring
- S1-S14 harm category breakdown with severity levels (critical/high/medium/low)
- Low-confidence alert system for uncertain classifications
- Cross-run safety trend tracking (
safety_trend.jsonl) - LLM-as-Judge scoring (API and local model support)
- Automated benchmark evaluation via lm-evaluation-harness
- Built-in adversarial prompt library (50 prompts across 8 categories)
- Human approval gate (
require_human_approval, exit code 4)
EU AI Act Compliance (Articles 9-17)
- Annex IV technical documentation generator
- Structured audit event log (
audit_log.jsonl) with hash chaining - Risk assessment declaration (risk level, domain, mitigations)
- Data governance reporting (source, quality, bias mitigation)
- Model integrity verification (SHA-256 checksums for all artifacts)
- Deployer instructions generator (Article 13)
- Evidence bundle export (ZIP archive for auditors)
- QMS SOP templates (5 documents: training, validation, monitoring, change, incident)
- Post-market monitoring configuration scaffold
Model Capabilities
- MoE fine-tuning support (expert quantization, selective training)
- Multimodal VLM pipeline detection
- Model merging: TIES, DARE, SLERP, linear interpolation
- Advanced PEFT methods: PiSSA, rsLoRA, DoRA
- Automatic model card generation (HuggingFace format)
CLI & UX
--wizardinteractive config generator with GPU detection--dry-runconfig validation (JSON and text output)--benchmark-onlyevaluate existing models without training--mergestandalone model merging--compliance-exportgenerate audit artifacts--quietsuppress INFO logs--offlineair-gapped mode (HF_HUB_OFFLINE)--resumecheckpoint resume (auto-detect or explicit path)--output-format jsonmachine-readable output--log-levelconfigurable logging- Exit codes: 0 (success), 1 (config error), 2 (training error), 3 (eval failure), 4 (awaiting approval)
Infrastructure
- Docker multi-stage build + docker-compose (training + TensorBoard)
- CI pipeline: 3 parallel jobs (lint, test matrix 3.10/3.11/3.12, validate)
- Ruff linting + formatting enforced
- 242 unit tests across 20 test files
- Branch protection rules on main
- GitHub issue templates (bug report, feature request) + PR template
- Apache License 2.0
- CONTRIBUTING.md + CODE_OF_CONDUCT.md
Documentation
- 6 user guides (quickstart, alignment, CI/CD, enterprise, safety, troubleshooting)
- 5 Colab-ready notebooks (SFT, DPO, KTO, GRPO, multi-dataset)
- Full EN/TR documentation (architecture, configuration, usage, roadmap)
- Structured logging (
loggingmodule) replaces allprint()calls - Config validation via Pydantic v2 with
extra="forbid"(typos caught) trust_remote_codenow configurable via YAML (default: false)bf16/fp16auto-detected based on GPU capabilityno_cudareplaced withuse_cpu(HF deprecation)device_mapuses{"": 0}on single GPU without 4-bit (prevents model splitting)gradient_checkpointingauto-disabled on CPUnum_procfor dataset processing scales with CPU countenable_input_require_gradsalways called for LoRA compatibility- Dependency upper bounds pinned to prevent breaking changes
max_length→max_seq_lengthfor TRL SFTConfig compatibilitytextcolumn datasets supported without reformatting
- 54 code review findings resolved (4 critical, 12 high, 19 medium, 14 low)
- Silent exception handling eliminated across all modules
- MoE expert quantization no longer corrupts weights (was using int8 cast)
- SLERP merge saves/restores base state correctly
- Webhook sanitizes metrics to numeric values only
- DARE merge handles
drop_rate >= 1.0without division by zero - Early stopping callback only added when validation data exists
- Audit log uses hash chaining for tamper evidence
- Model integrity hashes all files recursively (not just top-level)
- Checkpoint cleanup only removes
checkpoint-*dirs (not entire output_dir)
- Initial release
- SFT fine-tuning with TRL SFTTrainer
- LoRA/QLoRA (4-bit NF4) via PEFT
- Unsloth backend support
- DoRA adapter support
- YAML-based configuration
- Webhook notifications (Slack/Teams)
- Model versioning
- Basic evaluation checks (max loss, baseline comparison)
- Auto-revert on quality degradation