Sapling SAP
Reference

The review rules

Every rule of the review, with what it asks, its severity, its fix and the guideline it comes from.

This page is the repository's own document docs/rules.md, shown as it is, so that it never drifts from the product.

Made by node tools/rules_doc.ts > docs/rules.md from the registry (src/rules.ts, the modules of src/rules/) and the knowledge base (knowledge/dist/knowledge.json): do not edit by hand. A rule runs on every sapc check, build, json and review, in the editor as you type, and in the MCP server. Each finding says what is missing or wrong, the fix in Sapling, and the source (the guideline and its section).

RuleFamilySeverityFires whenFixSource
gamble.16gambleerrora test (test, test="...", a model) gives a p-value, the front matter has no setting sapling: alpha-level: and the test no alpha= optionalpha-level: {level: 0.05, sided: 2, cite: ...} under sapling: in the front matterGamble et al. 2017, Table, item 16; ICH E9 5.5
gamble.20gambleerror (clauses: error, warning)no block population; or an analysis or table without population=; or a block population without definition= (warning); or tables of safety data (ADAE, ADVS, ADLB, ADEG, ADEX) and no population on ADSL.SAFFL (warning)::: {.population #SAF condition="ADSL.SAFFL" definition="..."} with the name "Safety population" in its body and []{.cite source="..."} after it; population="SAF" on the safety tablesGamble et al. 2017, Table, item 20; ICH E9 5.2
gamble.17gamblewarningtwo or more primary estimands, or the protocol (USDM) names two or more primary endpoints, or a primary model compares more than one active group with control (contrast=pairwise), or a key secondary estimand; and no block multiplicity (one with procedure=none counts only with its reason or citation)::: {.multiplicity #MULT procedure="hierarchy"} with the title in bold, then [...]{.hypothesis #H1 analysis="P_ADAS"} ..., then :::Gamble et al. 2017, Table, item 17; ICH E9 5.6; FDA Multiple Endpoints in Clinical Trials 2022 I
gamble.18gamblewarningan analysis (not of ADSL baseline data) has a test with a p-value, and neither the test nor the summary has a 95% CI::: {.model stats="Estimate = xx.xx; 95% CI = (xx.xx, xx.xx); p-value = x.xxx"} (or a Diff and 95% CI row)Gamble et al. 2017, Table, item 18
gamble.28gamblewarningthe analysis of a primary estimand has no ::: {.missingdata #ID analysis="..."} and its model is not an MMRM (a likelihood method under MAR)::: {.missingdata #MD1 analysis="P_ADAS" assumption="MAR" handling="..."} with the justification "why this handling fits the estimand" in its bodyGamble et al. 2017, Table, item 28; NRC report on missing data 2010, Recommendation 10; EMA missing data guideline 2010 6
e9r1.attributese9r1error (clauses: error, warning)an estimand without population="...", treatment or endpoint (error); without summary and without an analysis with a model to write it from (warning); without intercurrent (warning)::: {.estimand #EST1 population="EFF" analysis="P_ADAS"} with the title in bold, then [...]{.treatment} [...]{.endpoint} [event]{.intercurrent strategy="strategy"}, then :::ICH E9(R1) A.3.3
e9r1.strategiese9r1errorthe strategy of a part intercurrent is not treatment policy, hypothetical, composite, while on treatment or principal stratum[event]{.intercurrent strategy="treatment policy"} (or hypothetical, composite, while on treatment, principal stratum)ICH E9(R1) A.3.2
e9r1.treatment-policy-terminale9r1errora part intercurrent event names death (died, fatal) with the strategy treatment policy, and the endpoint is not a time to event (no "time to", "survival", no Surv() model)[Death]{.intercurrent strategy="composite"} (death as a bad outcome), or the strategy while on treatmentICH E9(R1) A.3.2
e9r1.main-estimatore9r1errora primary estimand (level=primary, or the first estimand) has no analysis="..."; or the population of an estimand is not the population of its analysis::: {.estimand #EST1 level="primary" population="EFF" analysis="P_ADAS"} with the title in boldICH E9(R1) A.5.1
practice.estimand-analysis-consistencypracticeerror (clauses: error, warning)an estimand has an intercurrent event with treatment policy, and its analysis keeps only on-treatment records: a condition on ONTRTFL (or another on-treatment flag), or on an ANLzzFL whose derivation (ADaM specification, derivation) drops the records after discontinuation (the warning clause; the error clause, a population that differs, is e9r1.main-estimator)keep the records after the event (treatment policy), or change the strategy ([...]{.intercurrent strategy="while on treatment"} or hypothetical) with its missing-data handlingICH E9(R1) A.2, A.5.1; Kang et al. 2022, Proposed template
practice.as-randomised-as-treatedpracticewarningan efficacy table (efficacy data, or the analysis of an estimand) has columns by an actual-treatment variable (TRTA, TRTxxA), or a safety table by a planned one (TRTP, TRTxxP); also a primary model whose treatment term is actualtreatments: {groups: ..., variable: ADSL.TRT01P} under sapling: for efficacy (columns=<a grouping on TRT01A> for safety), or a block waive with the reasonICH E9 5.2.1, 6.3
method.mmrm/covariance_structuremethodserrora model with test mmrm has no covariance term (us(AVISIT | USUBJID), toep(...), ...) and no covariance= optioncovariance="us" on the model, or the term us(AVISIT | USUBJID) in the formulaMallinckrodt et al. 2008 (MMRM recommendations), Recommendations; R: mmrm 2026, mmrm() and covariance structures; SAS/STAT User's Guide 2026, The MIXED Procedure (REPEATED statement, DDFM= option)
method.mmrm/df_methodmethodserrora model with test mmrm has no df= option (df="open": the plan leaves it open, an open decision)df="kenward-roger" (or satterthwaite, residual, between-within)Mallinckrodt et al. 2008 (MMRM recommendations), Recommendations; R: mmrm 2026, mmrm() and covariance structures; SAS/STAT User's Guide 2026, The MIXED Procedure (REPEATED statement, DDFM= option)
method.mmrm/fallback_ordermethodswarninga model with test mmrm and an unstructured covariance (us) has no fallback= listfallback="toeph; ar1h; ar1"Mallinckrodt et al. 2008 (MMRM recommendations), Recommendations; R: mmrm 2026, mmrm() and covariance structures; SAS/STAT User's Guide 2026, The MIXED Procedure (REPEATED statement, DDFM= option)
method.cox/tiesmethodserrora model with test cox has no ties= option (ties="open": the plan leaves it open, an open decision)ties="efron" (R survival default; SAS PROC PHREG uses breslow)Cox 1972, Abstract; R: survival package 2026, coxph, cox.zph; SAS/STAT User's Guide 2026, The PHREG Procedure, MODEL statement TIES= option
method.km-logrank/censoring_rulesmethodserroran analysis with a Surv(AVAL, CNSR) response has no block derivation of the censoring variable (CNSR) of its data set, and the ADaM the plan derives does not make it (a timetoevent block)::: {.derivation #CNSR variable="ADTTE.CNSR" rule="0 = the event; 1 = alive at the last contact date; ..."} and []{.cite source="..."} after itKaplan and Meier 1958, Abstract; ADaM BDS for time-to-event analyses 2012, Scope and variables; R: survival package 2026, survfit.formula, survdiff
fda-mult.co-primarymultiplicitywarning (clauses: warning, info)a ::: {.multiplicity #ID procedure="co-primary"} has a hypothesis tested at an alpha above the overall alpha (warning); no block samplesize states joint_power= (info)test each co-primary hypothesis at the overall alpha; ::: {.samplesize #ID n="..." joint_power="0.80"}FDA Multiple Endpoints in Clinical Trials 2022 III.A.1, III.B, III.C.1
interim.alpha-spendinginterimerror (clauses: error, warning)an interim block with an efficacy purpose has no spending= and no alpha= or z= (error); it has no information= (warning); the looks of a hypothesis of hypotheses= spend in all, with the boundaries the plan states (alpha=, z=, final_alpha=), more than the level of the hypothesis in its multiplicity block, at the precision of that level and one more decimal (error)spending="obrien-fleming" (or pocock, hwang-shih-decani, haybittle-peto, fixed with alpha=) and information="0.5" on the interim; hypotheses="H1" names the hypotheses it tests; stated numbers that the boundaries of the compiler giveLan and DeMets 1983, Abstract; O'Brien and Fleming 1979, Abstract; Pocock 1977, Abstract
e9.covariates-subgroupssubgroupswarninga subgroup block has no interaction= (no test of the interaction of the treatment and its factors, and no stated reason: interaction="none" with the reason in its body)interaction="0.10" on the subgroup block (the level of the test of the treatment-by-factor terms), or interaction="none" with the reasonICH E9 5.7
e9.multiplicitysubgroupswarninga subgroup block reports p-values (pvalue="true", or an interaction test) and the plan has no multiplicity block with procedure="none" (all other tests nominal) and no hypothesis that names one of its analyses::: {.multiplicity #M_NOMINAL procedure="none"} with the reason (the subgroup p-values are nominal and support no claim), or the subgroup analysis as a hypothesis of a multiplicity blockICH E9 5.6
ema-subgroups.prespecifysubgroupswarninga subgroup block has no level= (key or exploratory); a factor of it is a grouping without groups= (its levels from the data)level="exploratory" (or key) on the subgroup block; groups="<65; 65-80; >80" on the grouping of each factorEMA subgroup guideline 2019 6.1, 6.2; ICH E9 5.7
safety.teae-definitionsafetywarningan incidence (or an analysis) selects the treatment-emergent events (a flag TRTEMFL) and the plan has no treatmentemergent block, no derivation of the flag (derivation, flag or derive of it) and no sentence that defines treatment-emergent::: {.treatmentemergent #TEAE end="TRTEDT + 30" missing="emergent"} (the period: from the first dose to 30 days after the last dose; an event with a missing start date counts) with its citationFDA Standard Safety Tables and Figures Integrated Guide 2025 1.2; ICH E9 6.3; ICH E3 12.2.2
safety.ae-by-soc-ptsafetywarningan incidence with rows by system organ class or preferred term has no order= (frequency, frequency in a group, or alphabetical)order="frequency" (the most subjects first; order="frequency in <group>") or order="alphabetical" on the incidenceICH E3 12.2.2; ICH E9 6.2; FDA Standard Safety Tables and Figures Integrated Guide 2025 2.2
safety.ae-severity-relatednesssafetyinfoan analysis has rows by a severity or grade variable (AESEV, ASEV, AETOXGR, ATOXGR) without worst= on it; or an incidence with worst= on a toxicity grade and the plan has no dictionary CTCAE with its versionworst="<the grouping of the severity>" on the incidence (or the analysis), its groups in order; ::: {.dictionary name="CTCAE" version="5.0" for="toxicity grades"}ICH E3 12.2.2; ICH E9 6.3
safety.lab-shiftsafetyinfoan analysis has rows by a category at baseline (BNRIND, BTOXGR) and after it (ANRIND, ATOXGR), no grouping by visit, and no worst= on the category after baselineworst="<the grouping of the category after baseline>" on the analysis (its groups in order), or a grouping by the analysis visitICH E3 12.4.2.2
safety.lab-pcsasafetywarningan analysis of laboratory data (ADLB...) has rows by a toxicity grade (ATOXGR, BTOXGR) and the plan has no dictionary CTCAE (or another grading scale) with its version::: {.dictionary name="CTCAE" version="5.0" for="laboratory toxicity grades"}ICH E3 12.4.2.3; ICH E9 6.3, 6.4
safety.descriptivesafetyinfoan incidence has a p-value (a test of each row, or a comparison with its p-value) and its table has no footnote that says descriptive, nominal or flag, and the plan has no multiplicity block with procedure="none"^[The p-values are descriptive: they are not adjusted for multiplicity.] in the table, or pvalue="false" on a comparison of each rowICH E9 6.4; FDA Standard Safety Tables and Figures Integrated Guide 2025 1.3
safety.aesisafetyinfoan incidence whose title names an event of special interest selects its events with no query variable (SMQzzNAM, CQzzNAM, a query flag) and no list of terms (AEDECOD in (...))condition='ADAE.TRTEMFL = "Y" and ADAE.CQ01NAM = "..."' (the query) or '... and ADAE.AEDECOD in ("...", "...")' (the terms)FDA Standard Safety Tables and Figures Integrated Guide 2025 2.2; ICH M11 template 10.7
missing.single-imputationmissingwarningthe analysis of a primary estimand takes LOCF, BOCF or WOCF records (DTYPE in its records, a block derivation so named, or a DTYPE of its data set with these values that the records do not exclude), and no ::: {.missingdata #ID analysis="..."} gives a justification::: {.missingdata #MD1 analysis="P_ADAS" assumption="..." handling="locf"} with the justification "why LOCF fits this estimand" in its body; and an MNAR sensitivityNRC report on missing data 2010, Recommendation 10; EMA missing data guideline 2010 6.3.1
fda-cov.stratification-factorscovariateswarning::: {.design type="..." strata="..."} names a stratification factor that is not a term (factor, covariate or strata) of the model of a primary analysisadd the factor to the model (C(STRAT) or strata(STRAT)), or a block waive with the reasonFDA covariate adjustment 2023 III.A; EMA covariate adjustment 2015 5.2; ICH E9 5.7
ema-cov.post-randomisationcovariateserrora covariate or factor of a primary model is an analysis value (AVAL, CHG, ...) or an ADSL variable of the treatment period (duration of exposure, completion, discontinuation, dose)use the baseline value (BASE, a *BL variable), or remove the termEMA covariate adjustment 2015 5.5; FDA covariate adjustment 2023 II; ICH E9 5.7

The built-in rules (phase 1)#

RuleSeverityFires whenFixSource
estimand-estimatorwarningan estimand that is not primary has no analysis="..."analysis="ID" on estimandICH E9(R1), A.3.2: the estimator is aligned with the estimand
primary-has-estimandinfoan analysis has a model and no estimand, sensitivity or supplementary analysis names it::: {.estimand #ID analysis="ID"} with the title in boldICH E9(R1), A.5.1

What each rule asks, in the words of the knowledge base#

Writing a rule#

A family module of src/rules/ exports rules: KRule[]: id (a knowledge item id, or item/key of a method page's specify), describe, condition, fix, and check(c, kb), which returns kb.find(id, span, message, fix, words): the severity and the source come from the knowledge base (words choose the clause of the item's check). Register the module in src/rules.ts, add a test that makes it fire and one that passes (test/rules.test.ts), and run node tools/rules_doc.ts > docs/rules.md. A company's own rules: sapc check --rules module.mjs (the Rule interface of src/rules.ts).