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).
- Severity comes from the knowledge item (or the
specifyentry of a method page):error(the SAP cannot be executed or is ambiguous for the primary analysis),warning(a guideline expects it),info(good practice). Where the item's check has clauses of different levels (ERROR when ... WARNING when ...), the finding takes the level of its clause. Without a knowledge base the rules still run, as warnings, without sources. - The primary estimand is the estimand with
level="primary"; when no estimand says its level, the first estimand of the plan. Its analysis (analysis="...") is the primary analysis. - A plan silences a rule only with a reason:
::: {.waive rule="rule-id" block="ID"}with the reason in its body. The finding stays as information with the reason; the PDF and the preview show the waiver. A waiver ofmethod.mmrmcovers its keys (method.mmrm/df_method, ...). A waiver of a rule that finds nothing is a warning; of an unknown rule an error. - The proposal of the knowledge base (
docs/knowledge.md, "Lint rules for the next phase") is implemented as the 20 rules of the families below; the two built-in rules of phase 1 stay.
| Rule | Family | Severity | Fires when | Fix | Source |
|---|---|---|---|---|---|
gamble.16 | gamble | error | a test (test, test="...", a model) gives a p-value, the front matter has no setting sapling: alpha-level: and the test no alpha= option | alpha-level: {level: 0.05, sided: 2, cite: ...} under sapling: in the front matter | Gamble et al. 2017, Table, item 16; ICH E9 5.5 |
gamble.20 | gamble | error (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 tables | Gamble et al. 2017, Table, item 20; ICH E9 5.2 |
gamble.17 | gamble | warning | two 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.18 | gamble | warning | an 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.28 | gamble | warning | the 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 body | Gamble et al. 2017, Table, item 28; NRC report on missing data 2010, Recommendation 10; EMA missing data guideline 2010 6 |
e9r1.attributes | e9r1 | error (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.strategies | e9r1 | error | the 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-terminal | e9r1 | error | a 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 treatment | ICH E9(R1) A.3.2 |
e9r1.main-estimator | e9r1 | error | a 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 bold | ICH E9(R1) A.5.1 |
practice.estimand-analysis-consistency | practice | error (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 handling | ICH E9(R1) A.2, A.5.1; Kang et al. 2022, Proposed template |
practice.as-randomised-as-treated | practice | warning | an 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 actual | treatments: {groups: ..., variable: ADSL.TRT01P} under sapling: for efficacy (columns=<a grouping on TRT01A> for safety), or a block waive with the reason | ICH E9 5.2.1, 6.3 |
method.mmrm/covariance_structure | methods | error | a model with test mmrm has no covariance term (us(AVISIT | USUBJID), toep(...), ...) and no covariance= option | covariance="us" on the model, or the term us(AVISIT | USUBJID) in the formula | 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/df_method | methods | error | a 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_order | methods | warning | a model with test mmrm and an unstructured covariance (us) has no fallback= list | fallback="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/ties | methods | error | a 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_rules | methods | error | an 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 it | Kaplan 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-primary | multiplicity | warning (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-spending | interim | error (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 give | Lan and DeMets 1983, Abstract; O'Brien and Fleming 1979, Abstract; Pocock 1977, Abstract |
e9.covariates-subgroups | subgroups | warning | a 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 reason | ICH E9 5.7 |
e9.multiplicity | subgroups | warning | a 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 block | ICH E9 5.6 |
ema-subgroups.prespecify | subgroups | warning | a 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 factor | EMA subgroup guideline 2019 6.1, 6.2; ICH E9 5.7 |
safety.teae-definition | safety | warning | an 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 citation | FDA Standard Safety Tables and Figures Integrated Guide 2025 1.2; ICH E9 6.3; ICH E3 12.2.2 |
safety.ae-by-soc-pt | safety | warning | an 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 incidence | ICH E3 12.2.2; ICH E9 6.2; FDA Standard Safety Tables and Figures Integrated Guide 2025 2.2 |
safety.ae-severity-relatedness | safety | info | an 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 version | worst="<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-shift | safety | info | an 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 baseline | worst="<the grouping of the category after baseline>" on the analysis (its groups in order), or a grouping by the analysis visit | ICH E3 12.4.2.2 |
safety.lab-pcsa | safety | warning | an 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.descriptive | safety | info | an 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 row | ICH E9 6.4; FDA Standard Safety Tables and Figures Integrated Guide 2025 1.3 |
safety.aesi | safety | info | an 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-imputation | missing | warning | the 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 sensitivity | NRC report on missing data 2010, Recommendation 10; EMA missing data guideline 2010 6.3.1 |
fda-cov.stratification-factors | covariates | warning | ::: {.design type="..." strata="..."} names a stratification factor that is not a term (factor, covariate or strata) of the model of a primary analysis | add the factor to the model (C(STRAT) or strata(STRAT)), or a block waive with the reason | FDA covariate adjustment 2023 III.A; EMA covariate adjustment 2015 5.2; ICH E9 5.7 |
ema-cov.post-randomisation | covariates | error | a 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 term | EMA covariate adjustment 2015 5.5; FDA covariate adjustment 2023 II; ICH E9 5.7 |
The built-in rules (phase 1)#
| Rule | Severity | Fires when | Fix | Source |
|---|---|---|---|---|
estimand-estimator | warning | an estimand that is not primary has no analysis="..." | analysis="ID" on estimand | ICH E9(R1), A.3.2: the estimator is aligned with the estimand |
primary-has-estimand | info | an analysis has a model and no estimand, sensitivity or supplementary analysis names it | ::: {.estimand #ID analysis="ID"} with the title in bold | ICH E9(R1), A.5.1 |
What each rule asks, in the words of the knowledge base#
gamble.16(16. Level of statistical significance): Every test has its significance level. The knowledge itemgamble.16(knowledge/checklists/gamble-2017.md:406).gamble.20(20. Analysis populations): Every analysis has a population, every population a definition in words, safety analyses a safety population. The knowledge itemgamble.20(knowledge/checklists/gamble-2017.md:527).gamble.17(17. Multiplicity): Two or more confirmatory hypotheses need a multiplicity strategy. The knowledge itemgamble.17(knowledge/checklists/gamble-2017.md:423).gamble.18(18. Confidence intervals): A comparison of treatments reports an estimate with its confidence interval, not a p-value only. The knowledge itemgamble.18(knowledge/checklists/gamble-2017.md:445).gamble.28(28. Missing data): The primary analysis says how it handles missing data, and under which assumption. The knowledge itemgamble.28(knowledge/checklists/gamble-2017.md:839).e9r1.attributes(The five attributes of an estimand): An estimand has its five attributes: population, treatment, variable, intercurrent events with strategies, population-level summary. The knowledge iteme9r1.attributes(knowledge/guidelines/ich-e9r1.md:18).e9r1.strategies(The five strategies): Each intercurrent event has one of the five strategies of ICH E9(R1). The knowledge iteme9r1.strategies(knowledge/guidelines/ich-e9r1.md:62).e9r1.treatment-policy-terminal(Treatment policy cannot handle terminal events): Treatment policy cannot handle a terminal event (death): the variable does not exist after it. The knowledge iteme9r1.treatment-policy-terminal(knowledge/guidelines/ich-e9r1.md:82).e9r1.main-estimator(The main estimator is aligned with the estimand): A primary estimand names its main estimator, and the estimator uses the estimand's population. The knowledge iteme9r1.main-estimator(knowledge/guidelines/ich-e9r1.md:184).practice.estimand-analysis-consistency(The analysis matches the estimand): The records of the main analysis match the strategies of its estimand. The knowledge itempractice.estimand-analysis-consistency(knowledge/practice/conventions.md:11).practice.as-randomised-as-treated(As randomised for efficacy, as treated for safety): Efficacy by the randomised (planned) treatment, safety by the actual treatment. The knowledge itempractice.as-randomised-as-treated(knowledge/practice/data-handling.md:37).method.mmrm/covariance_structure(The covariance structure of the within-patient errors (usually unstructured), and whether it is the same in each treatment group.): An MMRM states its covariance structure. The knowledge itemmethod.mmrm(knowledge/methods/mmrm.md:14).method.mmrm/df_method(The method for the denominator degrees of freedom (Kenward-Roger is usual; Satterthwaite is an alternative).): An MMRM states its method for the denominator degrees of freedom. The knowledge itemmethod.mmrm(knowledge/methods/mmrm.md:14).method.mmrm/fallback_order(The ordered list of structures to use if the unstructured model does not converge (for example heterogeneous Toeplitz, heterogeneous AR(1), Toeplitz, AR(1), compound symmetry) and the convergence criterion.): An MMRM with an unstructured covariance gives the order of structures to try when it does not converge. The knowledge itemmethod.mmrm(knowledge/methods/mmrm.md:14).method.cox/ties(The method for tied event times (Efron or Breslow or exact).): A Cox model states how it handles tied event times. The knowledge itemmethod.cox(knowledge/methods/cox.md:13).method.km-logrank/censoring_rules(Each censoring reason with its censoring date (last known alive, last adequate assessment, data cut-off, new anticancer therapy).): A time-to-event analysis has the derivation of its censoring. The knowledge itemmethod.km-logrank(knowledge/methods/km-logrank.md:14).fda-mult.co-primary(Co-primary endpoints: no alpha adjustment, but less power): Co-primary endpoints (all must be significant) are each tested at the overall alpha, and the sample size gives their joint power. The knowledge itemfda-mult.co-primary(knowledge/guidelines/multiplicity.md:41).interim.alpha-spending(Efficacy looks: boundaries from an alpha spending function): An interim analysis that may stop for efficacy has boundaries that keep the overall type I error: a spending function or fixed boundaries, the information fraction of each look, and an alpha that its family accounts for. The knowledge iteminterim.alpha-spending(knowledge/guidelines/interim-adaptive.md:37).e9.covariates-subgroups(Covariates, subgroups and interactions): Subgroup analyses test the interaction of the treatment and each factor first (or say why they do not), and are read with care. The knowledge iteme9.covariates-subgroups(knowledge/guidelines/ich-e9.md:356).e9.multiplicity(Adjustment of significance and confidence levels): The p-values of subgroup analyses (of each level, of the interaction) come with a statement on multiplicity: they are nominal, or a procedure controls them. The knowledge iteme9.multiplicity(knowledge/guidelines/ich-e9.md:339).ema-subgroups.prespecify(Plan key and exploratory subgroups): A subgroup analysis says whether it is key or exploratory, and its factors have their levels (cut-points or categories) before unblinding. The knowledge itemema-subgroups.prespecify(knowledge/guidelines/subgroups.md:15).safety.teae-definition(Definition of treatment-emergent): The plan defines treatment-emergent: the start and the end of the period, and how an event with a missing start date counts. The knowledge itemsafety.teae-definition(knowledge/safety/adverse-events.md:16).safety.ae-by-soc-pt(Adverse events by system organ class and preferred term): An adverse event table by system organ class and preferred term states the order of its rows. The knowledge itemsafety.ae-by-soc-pt(knowledge/safety/adverse-events.md:74).safety.ae-severity-relatedness(Severity, relationship, and events leading to discontinuation): A table by severity or toxicity grade counts a subject with several events of a term once, at the worst; a toxicity grade names its scale and version. The knowledge itemsafety.ae-severity-relatedness(knowledge/safety/adverse-events.md:92).safety.lab-shift(Shift tables): A shift table says which value after baseline it uses: the worst, the last, or each visit. The knowledge itemsafety.lab-shift(knowledge/safety/labs-vitals-ecg.md:34).safety.lab-pcsa(Potentially clinically significant values): Laboratory values counted by a toxicity grade name the grading scale and its version. The knowledge itemsafety.lab-pcsa(knowledge/safety/labs-vitals-ecg.md:50).safety.descriptive(Safety analyses are descriptive): A test in an adverse event table is said to be descriptive (a flag, not a claim), in a footnote of the table or in a multiplicity block that says the other tests are nominal. The knowledge itemsafety.descriptive(knowledge/safety/exposure-disposition.md:74).safety.aesi(Grouped terms and adverse events of special interest): An adverse event of special interest names its definition: a query (an SMQ or a custom query) or its list of terms. The knowledge itemsafety.aesi(knowledge/safety/adverse-events.md:144).missing.single-imputation(Single imputation (LOCF, BOCF) needs a justification): A primary analysis on single-imputed data (LOCF, BOCF, WOCF) justifies it. The knowledge itemmissing.single-imputation(knowledge/guidelines/missing-data.md:72).fda-cov.stratification-factors(Adjust for the stratification factors): The primary model adjusts for the stratification factors of the randomisation. The knowledge itemfda-cov.stratification-factors(knowledge/guidelines/covariates.md:35).ema-cov.post-randomisation(Do not adjust for post-randomisation variables): A primary model adjusts only for values measured before randomisation. The knowledge itemema-cov.post-randomisation(knowledge/guidelines/covariates.md:53).
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).