ICH Guidelines Reference
This reference explains which guideline family is relevant to a MolTrace review and what the user should do with the threshold result.
Coverage
Section titled “Coverage”- Q3A(R2): organic impurities in new drug substances
- Q3B(R2): impurities in new drug products
- Q3C: residual solvents
- Q3D: elemental impurities
- M7(R2): mutagenic impurities
- Q2(R2) and Q14: analytical validation and development
Guideline tables and jurisdiction-specific language require regulatory-owner review before production publication.
Source verification workflow
Section titled “Source verification workflow”Do not invent threshold numbers. For each guideline page, the regulatory owner must:
- Download the current official guideline from ICH or the relevant agency site.
- Extract the threshold table and page reference.
- Record the effective date and document version.
- Compare FDA, EMA, PMDA, and Health Canada interpretations where they differ.
- Add a reviewer note explaining any MolTrace-specific implementation choice.
Threshold table template
Section titled “Threshold table template”| Guideline | Context | Threshold type | Value | Source page | Reviewer |
|---|---|---|---|---|---|
| Q3A(R2) | New drug substance impurity | Reporting | TBD after source verification | TBD | Regulatory owner |
| Q3A(R2) | New drug substance impurity | Identification | TBD after source verification | TBD | Regulatory owner |
| Q3A(R2) | New drug substance impurity | Qualification | TBD after source verification | TBD | Regulatory owner |
| Q3C | Residual solvent | Class-specific limit | TBD after source verification | TBD | Regulatory owner |
| M7(R2) | Mutagenic impurity | Acceptable intake or control class | TBD after source verification | TBD | Regulatory owner |
Jurisdiction comparison
Section titled “Jurisdiction comparison”Each jurisdiction note should answer:
- Does the agency use the ICH text directly or add local implementation guidance?
- Which submission sections or forms are affected?
- Are nitrosamine or mutagenic impurity expectations updated by separate guidance?
- What evidence does MolTrace show to justify the threshold used?
The published page must show the official source citation next to every numeric threshold.
Backend capabilities
Section titled “Backend capabilities”The Regentry backend implements the ICH impurity guidelines as deterministic, version-pinned calculators — no model is ever in the numeric path. Each engine encodes the published ICH criteria (transcribed from the official tables and cited on every result) and returns a content-hashed rule_set_version, so a threshold is auditable and reproducible. Every value is decision-support: it must be verified against the official ICH source and signed off by a qualified reviewer before any filing use. The release timeline gives chronological context.
ICH Q3A(R2) / Q3B(R2) impurity thresholds
Section titled “ICH Q3A(R2) / Q3B(R2) impurity thresholds”calculate_q3ab_thresholds(daily_dose_g, substance_type, route)computes the reporting, identification, and qualification thresholds for drug substances (Q3A) and drug products (Q3B) from the maximum daily dose. It resolves each ICH ”% or absolute, whichever is lower” rule to a single effective % for the dose (converting a µg/day or mg/day total-daily-intake cap to a percentage), flags which limit binds, and returns the dose band, regulatory basis, and table reference per value. The canonical multi-band Q3B tables (with µg-TDI caps) and Q3A>2 gqualification at 0.05% are encoded exactly. (v0.22.1, 2026-06-08)
ICH Q3C(R8) residual solvents
Section titled “ICH Q3C(R8) residual solvents”-
classify_solvent(...)resolves a solvent by name, CAS, or SMILES and assigns it to Q3C(R8) Class 1 (avoid), Class 2 (limit by permitted daily exposure), or Class 3 (low toxic potential), returning the systemic PDE, the Option-1 concentration limit (ppm), recommended analytical methods, and the basis / table reference.check_residual_solvent_limits(...)checks measured residual levels against the dose-scaled permitted limit (Option 2 for Class 2/3; the fixed Option-1 limit for Class 1) and returns pass / fail with a signed margin. A curated subset of Appendices 1–3 (all 5 Class 1, 18 common Class 2, 21 representative Class 3 = 44 solvents) is encoded; an unknown solvent returns an explicitmatched=false, never a guessed limit. (v0.22.2, 2026-06-08)Correction (v0.74.5, 2026-08-30). The dossier path reached this engine with no route gate, so a route ICH Q3C does not cover was still given Q3C limits — a limit the unified
impurities/assessendpoint refuses for the same product. Separately, the spectral-observation path reported its concentration limit unlabelled, and that figure is the Option 1 constant at a 10 g/day reference dose rather than the product’s own. Both are fixed below.
ICH Q3D(R2) elemental impurities
Section titled “ICH Q3D(R2) elemental impurities”-
get_element_pde(element, route)returns the Q3D(R2) permitted daily exposure (PDE) for an elemental impurity by administration route, with its class and the 30%-of-PDE control threshold;calculate_concentration_limit(...)gives the permitted product concentration at a daily dose (Option 1: PDE ÷ max daily dose); andrisk_assessment_report(...)produces a class-driven Q3D risk assessment over a product’s components and manufacturing equipment (Class 1 & 2A always assessed; 2B on intentional addition or equipment sourcing; 3 route-dependent). The oral / parenteral / inhalation PDEs (Table A.2.1, all 24 elements) are encoded; cutaneous / transcutaneous routes return an explicit “not encoded” (route_data_available=false), never a guessed PDE. (v0.22.3, 2026-06-08)Correction (v0.74.6, 2026-08-30). That is true of the engine and was not true of the dossier assessment built on it. A declared cutaneous route with a recorded dose skipped the permitted-concentration branch for want of route data, emitted no warning, and stored the verdict field at its
Falseinitialiser — so 10 000 ppm nickel on a cutaneous dossier was recorded as within limits. An undeclared route separately defaulted tooral, which is the most permissive route for all 24 elements. Both are fixed; see A measured level with no applicable limit.
ICH M7(R2) mutagenic impurities
Section titled “ICH M7(R2) mutagenic impurities”classify_m7(...)assesses a potential impurity under ICH M7(R2) using the five-class scheme of Mueller et al. (2006): a DNA-reactive structural-alert screen plus the dual-(Q)SAR rule, with experimental data overriding in-silico predictions, Cohort-of-Concern (CoC) handling, and the staged less-than-lifetime threshold of toxicological concern (TTC). It returns the class (1–5), the TTC or a compound-specific-AI flag, the in-silico concordance, the CoC flag, an expert-review flag, and a narrative for CTD Section 3.2.S.3.2. The decision logic (class assignment, dual-(Q)SAR rule, CoC handling, staged-TTC math) is pure and content-versioned — no model in the path; the only model-like component is the expert rule-based structural-alert SMARTS screen (a rule engine, not an LLM), used as a per-system default only when a formal (Q)SAR result is not supplied. Class 4 (alert shared with the drug substance) needs drug-substance context and is not auto-assigned; the curated alert set must be verified against the official M7(R2) guideline and qualified expert review before any filing use. (v0.22.4, 2026-06-08)
FDA CPCA nitrosamine potency (the M7 Cohort-of-Concern path)
Section titled “FDA CPCA nitrosamine potency (the M7 Cohort-of-Concern path)”classify_cpca(smiles, authority='FDA'|'EMA')implements the canonical FDA Carcinogenic Potency Categorization Approach (CPCA) — a deterministic structure-activity flowchart that scores an N-nitrosamine’s carcinogenic potency and assigns one of five potency categories, each with a recommended acceptable-intake (AI) limit. It derives the compound-specific AI that ICH M7 defers to for Cohort-of-Concern nitrosamines. The α-hydrogen scoring table, the 16 feature point-values, the flowchart, and the AI ladder (Category 1 = FDA 26.5 / EMA 18, then 100 / 400 / 1500 / 1500 ng/day) are transcribed verbatim from the FDA’s open-sourcefeaturize-nitrosaminesreference tool and the Aug-2023 NDSRI guidance; RDKit recognizes structure only.calculate_cumulative_risk(...)applies the FDA Rev-2 cumulative rule (sum(measured / AI) < 1). Every result is decision-support requiring qualified toxicologist / regulatory-affairs sign-off, never a regulatory determination. (v0.23.0, 2026-06-09)
A measured level with no applicable limit is undetermined, never a pass
Section titled “A measured level with no applicable limit is undetermined, never a pass”The engines above refuse to guess a limit, and that has always been true of the engine. It was not true of the record: several branches computed no limit, said so in a warning, and left the verdict field at its False initialiser — which reads as within limits to every machine consumer of the record. The prose said one thing and the stored value said the opposite.
Four releases closed that class of defect. They are documented together because they share one shape, and because two of them were introduced or missed by the commits meant to fix the others.
- A Q3C route the guideline does not cover was still given its limits.
POST /regulatory/impurities/assessdeclines a non-Q3C route and says so; the dossier path reached the same engine with no gate. Measured before the fix: acetonitrile on a cutaneous dossier returned a concentration limit of 410.0 ppm with the threshold triggered — a limit the sibling endpoint refuses for the same product. The claim this started from (“the default ignores the dossier route”) was wrong about the mechanism: the encoded Q3C table is one PDE per solvent with no route dimension, so the route changes no number, and simply passing it through — the obvious fix — would have regressed, silently losing the classification for every cutaneous dossier instead of telling anyone why. A dossier with no declared route is deliberately not gated. (v0.74.5, 2026-08-30) - The spectral Q3C limit did not say which limit it was.
resolve_observed_impurityreportedconcentration_limit_ppmunlabelled. That is the ICH Q3C Option 1 constant — the limit at a 10 g/day reference dose, not the product’s. Option 2 scales to the real dose, so the two differ bydose / 10: five times too permissive at 50 g/day, five times too strict at 2 g/day. The module’s existing refusals were all scoped to the numerator (no measured amount, therefore no verdict) and each one affirmatively warranted the limit as sound. It is now labelled, never recomputed — no dose is reachable in that chain, and defaulting one would be exactly the guessed limit the module exists to refuse.limit_basisis derived from the persisted Q3C class so it cannot drift from it, and takesoption_1_10g,option_2_dose_scaledorclass_1_fixed— Class 1 is labelled separately, because a fixed Appendix limit has no PDE to scale and calling it Option 1 would be its own false statement. (v0.74.5, 2026-08-30) - An assumed Q3D route may not decide a pass. An undeclared route silently took
oraland wrote it onto the assessment, so an undeclared route was indistinguishable from a declared one. Measured across the encoded table: oral is the most permissive route for all 24 elements, and the PDE differs across routes for 22 of them — nickel 200 µg/day oral against 5 inhalation, mercury 30 against 1. The default is kept, because withholding the assessment outright would be worse than performing it; what changed is that the record now says the route was assumed, and no verdict is asserted where the assumption could have produced it. Only a pass can be decided by the assumption — oral being the maximum PDE and permitted concentration monotonic in it, a level at or above the oral limit is at or above every other route’s, so an exceedance holds whatever the real route is. An earlier draft withheld the verdict on observation alone and so downgraded a confirmed exceedance to undetermined — the safety direction inverted, and neither of its two tests caught it because both used levels that pass under oral. Whether the assumption is load-bearing is read from the guidance table rather than applied as one blanket policy: lead is 5 µg/day on all three encoded routes and thallium 8, so those two keep their verdict. (v0.74.6, 2026-08-30) - A declared Q3D route with no encoded limit reported a pass. Pre-existing and the worse of the two. Because the Q3D(R2) cutaneous appendix is deliberately not encoded, the engine correctly returns
route_data_available=falsewith no PDE — but with a dose recorded, the permitted-concentration branch was skipped for want of route data and the missing-dose branch could not fire, so no warning was emitted and the verdict kept its initialiser. Measured before the fix: 10 000 ppm nickel on a cutaneous dossier was stored as within limits. Both withheld verdicts now setreview_required, because existing readers flag a row on threshold triggered or review required, and a withheld verdict leaving both unset would render as nothing to see. (v0.74.6, 2026-08-30) - The same shape, three more times, found by looking for it. An adversarial sweep over the two commits above found three further instances — a Q3C route the guideline does not cover, a Q3C solvent with no configured rule and no encoded entry, and a Q3D element outside the list of 24, measured at 9 000 ppm and recorded as within limits. That last branch left the loop before the “no limit means undetermined” guard forty lines below it, so the guard was half-applied inside the commit that introduced it. One of the tests was itself encoding the weakness: it asserted the verdict
is not True, whichFalsesatisfies. Scoped to rows carrying an observed value — a row with nothing measured keeps its existing meaning, since no verdict is at risk there. The vocabulary lesson is now enforced rather than restated:Q3D_ENCODED_ROUTESis published and used, so if the Q3D(R2) cutaneous appendix is ever encoded the guard follows the canonical list instead of silently narrowing. And the undeclared-route warning fires whenever the route was assumed, not only where a verdict was withheld — lead and thallium rightly keep their verdict, but a lead-only panel was asserting a pass while saying nothing whatever about having assumed the route. (v0.74.14, 2026-09-03) - The qualification now travels with the numbers. The residual-solvent summary is copied verbatim into the draft CTD Module 3 bundle and into the regulatory readiness roll-up, and neither carries the warnings with it — so the prose explaining that ICH Q3C does not cover a cutaneous route stayed behind while the rows travelled into a submission-shaped document. An undetermined verdict with no stated reason is its own gap: a reviewer reading the bundle cannot tell no limit applies to this route from something went wrong here.
routeandq3c_route_coverednow ride on the summary itself, andq3c_route_coveredon each declined row so a row read on its own is still self-describing. The elemental summary already carriedrouteandroute_assumedfor exactly this reason. Neither consumer needed changing, which is why the fix sits at the producer. (v0.75.3, 2026-09-03)
Rule-set versions are now ordered, not just content-addressed
Section titled “Rule-set versions are now ordered, not just content-addressed”Every identifier the platform carried for a rule set was a content address, and sha256 has no order — it answers “the same bytes” and never “newer”. The registry field that looked like the answer was a caller default of "1.0.0" applied to all five engines at once. Each engine now declares an ordered version beside the content it orders, routed through a function that computes the identity hash from the payload so the two cannot drift. Every published rule_set_version is byte-identical to before, checked per engine — stored regulatory results carry it as provenance, and changing one would have invalidated existing records.
The comparison refuses rather than guessing: six of its eight branches produce unknown, each naming the measure that failed — an absent version, an unparseable one, identical bytes declared at two versions, one version over two different contents. "conformal-v1" is not a version, and "1.10.0" sorts before "1.9.0" as a string while being newer. Published through GET /system/active-versions; see Backend / API Contract. (v0.78.0, 2026-08-22)
Release timeline
Section titled “Release timeline”A chronological summary; see each subsection above for substantive detail.
| Version | Date | Headline |
|---|---|---|
| v0.78.0 | 2026-08-22 | Ordered rule-set versions beside the content they address |
| v0.75.3 | 2026-09-03 | Route qualification travels into the CTD bundle with the rows |
| v0.74.14 | 2026-09-03 | Three more half-applied “no limit means undetermined” guards |
| v0.74.6 | 2026-08-30 | An assumed Q3D route may not decide a pass |
| v0.74.5 | 2026-08-30 | Q3C route gate on the dossier path; the Option 1 limit is labelled |
| v0.23.0 | 2026-06-09 | FDA CPCA nitrosamine potency classifier (M7 CoC path) |
| v0.22.4 | 2026-06-08 | ICH M7(R2) mutagenic-impurity classifier |
| v0.22.3 | 2026-06-08 | ICH Q3D(R2) elemental-impurity engine |
| v0.22.2 | 2026-06-08 | ICH Q3C(R8) residual-solvent classifier |
| v0.22.1 | 2026-06-08 | ICH Q3A/B impurity threshold calculator |