Skip to content

Public Documentation

Documentation for EpiAwarePackageTools's public interface.

Contents

Index

Public API

EpiAwarePackageTools.ADRegistry Type
julia
abstract type ADRegistry
julia
ADRegistry

The contract a package's AD-fixture module must satisfy to drive the harness.

A registry reg is any object (commonly a package's ADFixtures module) responding to:

  • scenarios(reg; with_reference = true, kwargs...) returning a vector of scenarios. Each scenario s exposes s.name::String, s.f, s.x, s.contexts (a tuple, possibly empty), and s.res1 (the ForwardDiff reference gradient, or nothing). This matches a DifferentiationInterfaceTest scenario. Extra keyword arguments (e.g. a package's own scenario-group selector) are forwarded from the runners' scenario_kwargs.

  • backends(reg) returning a vector of named-tuples (; name, backend), where backend is an ADTypes backend.

The remaining bookkeeping accessors are optional: a registry that owns no broken or skipped scenarios may omit them, and the harness treats the missing accessor as "none". Define them only when a package actually has such scenarios.

  • broken_scenario_names(reg) (optional) returning a collection of scenario names broken on every backend. Default: empty.

  • backend_broken_scenarios(reg) (optional) returning a Dict{String, Set{String}} of per-backend broken scenario names. Default: empty.

  • backend_skip_scenarios(reg) (optional) returning a Dict{String, Set{String}} of per-backend scenario names too unstable to run at all. Default: empty.

A package may implement these as plain functions taking the registry, or (the common case) expose them as zero-argument functions on a module and pass the module as reg; the harness calls reg.f(...) either way via property access.

This is a documentation-only marker; the harness duck-types on the methods above.


Fields

source
EpiAwarePackageTools.MANAGED_README_SECTIONS Constant
julia
MANAGED_README_SECTIONS

The standard sections the kit manages, in the order update renders them between the standard-sections markers (see scaffold).

The managed block is appended to a README that carries none of these sections yet, so a package-owned section (commonly ## License) can end up above, between, or below the sections the kit writes. Only the order within the block is the kit's to guarantee, and that is what test_readme_sections checks when the markers are present (#236).

source
EpiAwarePackageTools.STANDARD_README_SECTIONS Constant
julia
STANDARD_README_SECTIONS

The standard EpiAware README section structure, in order, used as the default required set by test_readme_sections.

The order mirrors the sections the kit itself renders into a managed README — Contributing, then the citation section (## How to cite), then Code of conduct — so a freshly scaffolded package passes the order = true check out of the box. The Contributing group therefore precedes the citing/license group by design; a README that hand-places a ## License or ## Supporting and citing section above Contributing must move it below to conform, rather than this order being flipped (flipping it would fail every fresh scaffold, whose managed block renders Contributing first).

Each entry is a tuple of accepted ##-heading texts (case-insensitive, substring match), and the check passes if any variant is present; the H1 title and the badge block (between the markers, refreshed by update) precede these and are checked separately. A package may title the equivalent section differently (e.g. "Getting started" vs "Usage"), so a tuple lists the accepted alternatives. Extend or relax it per package via the required keyword of test_readme_sections.

source
EpiAwarePackageTools.ad_backend_support_table Function
julia
ad_backend_support_table(reg; scenario_kwargs) -> String
julia
ad_backend_support_table(reg; scenario_kwargs = (;)) -> String

Render a Markdown table summarising, per registry backend, how many of the registry's scenarios it supports and which are declared broken or skipped.

One row per backends(reg) entry, with the scenario coverage count (supported/total) and the sorted names from the optional bookkeeping accessors (broken_scenario_names, backend_broken_scenarios, backend_skip_scenarios; a missing accessor means none — see ADRegistry). The scaffolded AD-backends docs page calls this at docs-build time, so a package's broken-scenario declarations live only in its registry and the published support table can never drift from what the gradient tests actually mark broken.

scenario_kwargs is forwarded to the registry's scenarios call, as in test_working_backend.

source
EpiAwarePackageTools.DocsBuild.build_docs Function
julia
build_docs(mod; repo, authors, pages, deploy_url=nothing,
           skip_notebooks=false, tutorials_subdir, light_tutorials=[],
           heavy_tutorials=[], tutorial_stubs=[], force_stub_tutorials=[],
           linkcheck_ignore=[], index_rewrites=[], readme_execute=true,
           index_strip_sections=[], benchmark_page=true,
           history_suites=[], history_commits=5,
           history_regression_threshold=1.1, extra_modules=[],
           extra_remotes=Dict(), build_vitepress=true, deploy=true)

Run the standard EpiAware documentation build for package module mod. All paths derive from pkgdir(mod), so the managed docs/make.jl only forwards the package-owned config. Generates the home page, release notes, benchmark page, and API pages, processes the Literate tutorials, then renders with DocumenterVitepress and (when deploy) deploys. Under skip_notebooks, light tutorials still render in-process (cheap: seconds, not minutes) and only the heavy tutorials fall back to tutorial_stubs heading stubs — the flag exists to skip the slow ones, not the cheap ones. Independent of skip_notebooks, any heavy_tutorials entry named in force_stub_tutorials never executes and always renders from its tutorial_stubs heading — for a heavy tutorial with an unresolved problem of its own (e.g. a model that does not terminate in reasonable time), so it need not block its siblings from running for real. deploy=false builds without deploying, and build_vitepress=false runs Documenter without the final VitePress (npm) pass — both used by tests and fast local content builds. On the benchmark page, history_suites (when non-empty) restricts the overall summary and detail to the named headline suites, history_commits caps both to that many most-recent revisions, and history_regression_threshold sets the overall summary's regression-flag cutoff (see _embed_benchmark_history).

The owning modules of mod's re-exported docstrings are auto-discovered (see api_owning_modules) and folded into Documenter's modules so the generated @docs blocks for those re-exports resolve (#175); extra_modules adds any further owner modules auto-discovery cannot reach (e.g. a re-export referenced only from prose). Because Documenter drives its missing-docstring completeness check off the same modules list — with no working way to scope that check while widening @docs resolution — the completeness check is disabled whenever the resolution set is widened, so a package is never held responsible for a dependency's own missing-docstring hygiene (mod's own completeness is already guaranteed by construction: api_bindings emits every docstring mod owns).

Each owning module also needs a source remote, which Documenter cannot derive for a dependency installed from a git URL (#190). api_remotes derives one from the dependency's recorded git source URL and passes it to Documenter's remotes; extra_remotes supplies the rest, mapping a Module or a path to an "Org/Repo.jl" string or to anything Documenter's remotes accepts (e.g. Dict(SomeDep => "EpiAware/SomeDep.jl")).

source
EpiAwarePackageTools.check_broken Function
julia
check_broken(scenarios_list, backend; rtol, atol)
julia
check_broken(scenarios_list, backend; rtol = 5e-2, atol = 1e-6)

Run each scenario through plain DifferentiationInterface.gradient and record whether it matches its reference.

A scenario passes (@test true) when the gradient is a finite vector matching scen.res1 within tolerance, and is marked @test_broken otherwise. This lets a partial backend record the coverage it does have without an all-or-nothing result. DifferentiationInterface must be loaded by the caller.

source
EpiAwarePackageTools.dynamicppl_model_filter Function
julia
dynamicppl_model_filter(report) -> Any
julia
dynamicppl_model_filter(report) -> Bool

A report_filter for test_jet that drops reports arising inside a DynamicPPL @model-generated method (matched on the model-evaluator signature (::Model, ::AbstractVarInfo, ...)), and keeps every other report.

Use this for a Turing/DynamicPPL package whose public surface is @model functions: test_jet(MyPkg; report_filter = dynamicppl_model_filter). Without it, JET reports a false UndefVarErrorReport for every ~-assigned variable (and MethodErrorReports through the := tracker), none of which is a real defect.

source
EpiAwarePackageTools.on_surface_ambiguities Function
julia
on_surface_ambiguities(
    mod::Module,
    extname::Symbol;
    prefixes
) -> Vector{Tuple{Method, Method}}
julia
on_surface_ambiguities(mod, extname; prefixes = (string(nameof(mod)),))

The ambiguous method pairs over (mod, ext) that mod or its extension owns: both methods live in a module whose name starts with one of prefixes. This drops pairs owned by an unrelated third party (e.g. a ::Num overload from a Symbolics integration that collides with every concrete f(::Dist, ::Real)), keeping only ambiguities the package or its extension actually introduces.

prefixes defaults to the package name (which also covers its extensions, since extension modules are named <Package>...Ext); pass extra prefixes for trigger packages whose methods participate in a legitimate pair (e.g. ("MyPkg", "Distributions")).

source
EpiAwarePackageTools.raw_ambiguity_count Function
julia
raw_ambiguity_count(mod::Module, extname::Symbol) -> Int64
julia
raw_ambiguity_count(mod, extname)

Total (unfiltered) method-ambiguity count over (mod, ext), where ext is the loaded extension named extname. Useful as a sanity check that third-party phantom ambiguities are present before test_ext_ambiguities filters them out (so the on-surface filter is doing real work, not trivially empty).

source
EpiAwarePackageTools.scaffold Function
julia
scaffold(
    target_dir::AbstractString;
    force,
    ad,
    benchmarks,
    downgrade_compat,
    kwargs...
) -> @NamedTuple{created::Vector{String}, updated::Vector{String}, preserved::Vector{String}, removed::Vector{String}, readme::Symbol, license::Symbol, workspace::Symbol, gitignore::Symbol, logo::Symbol, standard_sections::Symbol, citation::Symbol, org_branding::Symbol, warnings::Vector{String}}
julia
scaffold(target_dir; force = false, ad = true, benchmarks = nothing,
    kwargs...)

Adopt the standard EpiAware package tooling in target_dir (a package root).

Writes the shipped standard configuration and test infrastructure so a package adopts the whole kit in one call. Two kinds of file are written:

  • managed standard infra — always written (overwriting any existing copy): root dev config (Taskfile.yml, .pre-commit-config.yaml, .JuliaFormatter.toml, .gitattributes, .secrets.baseline, codecov.yml), CI caller workflows + .github/dependabot.yml (which invoke the org reusables, including the opt-in per-backend ad.yaml matrix), and the test-infra drivers and isolated-env manifests (test/package/quality.jl, test/jet/runtests.jl + test/jet/Project.toml, test/formatter/runtests.jl + test/formatter/Project.toml, test/ad/setup.jl, test/ad/runtests.jl, benchmark/run.jl, benchmark/compare.jl).

  • package-owned skeletons — written only when absent, never overwritten: test/runtests.jl, test/Project.toml (the test env), test/package/ qa_config.jl (the QA config values the managed testset reads), LICENSE (the license-selected licence text — see below), NEWS.md (the hybrid-changelog seed), docs/src/assets/logo.svg (a placeholder logo — see the logo return value below), test/ad/scenarios.jl + test/ad/Project.toml, an ADFixtures registry skeleton implementing the ADRegistry contract (test/ADFixtures/Project.toml + src/ADFixtures.jl), benchmark/benchmarks.jl (the SUITE), and CITATION.cff (the citation metadata the managed "How to cite" README section points at — see the citation return value below). These are where a package's own unit tests, AD scenarios, registry, citation, and config values live.

The README body is package-owned, but three standard sections — Contributing, How to cite, and Code of conduct — are managed between the <!-- standard-sections:start -->/:end markers and refreshed on every sync (like the badge block), so their wording stays consistent across adopters. The citation content stays package-owned in CITATION.cff; the managed "How to cite" section only points at it.

Placeholders (, , , , , , ) are filled by scaffold_inputs: each defaults from the target Project.toml or a sensible org default and is overridable by keyword (e.g. scaffold(dir; org = "MyOrg")). No person, org, or repo name is hardcoded in any template.

LICENSE is package-owned and write-once: the license keyword (an SPDX id, one of MIT, Apache-2.0, default "MIT") selects the bundled licence text, written with / filled only when no LICENSE exists. update never rewrites it, so a package that deliberately changes its licence is not reverted on a sync.

The managed .github/workflows/Register.yml triggers Julia General Registry registration: a /register comment on an issue or PR, or a manual workflow_dispatch run, both post @JuliaRegistrator register on main's HEAD commit (gated on the actor having write/maintain/admin access). See setup_checklist for the rest of the one-off manual setup a fresh repo needs (Codecov, GitHub Pages, branch protection, ...).

ad controls whether the AD CI caller and AD test infrastructure are scaffolded, so a numerical package opts in and a tooling/non-numerical package opts out. It defaults to true (the common case for an EpiAware modelling package). When ad = true the managed AD-backends tutorial page (docs/src/getting-started/tutorials/ad-backends.jl, generalised from CensoredDistributions.jl) is also written: its body stays kit-current across syncs while the scenarios, backends, and broken/skip declarations it reports are read at docs-build time from the package-owned test/ADFixtures registry (rendered via ad_backend_support_table), and its registration plus docs-env deps are seeded into the package-owned docs/docs_config.jl, docs/pages.jl, and docs/Project.toml. When ad = false, none of the AD infra is written — no .github/workflows/ad.yaml, no test/ad/ drivers, scenarios, or env, no test/ADFixtures/ registry skeleton, no AD-backends docs page — and the files whose content depends on AD (Taskfile.yml, codecov.yml, test/Project.toml, and the docs seeds above) are emitted in their no-AD variants (no test-ad tasks, no per-backend ad-* coverage flags, no AD test/docs deps). Pass the same ad value to update to keep the standard stable.

benchmarks controls the opt-in benchmark suite: the benchmark CI callers (.github/workflows/benchmark.yaml, benchmark-history.yaml), the benchmark/ suite + compare script, and the docs benchmark page (its nav entry, the package-owned docs/benchmarks.md prose hook, and the package-owned docs/benchmarks_notes.md skipped/broken-benchmarks hook, gated by docs_config's BENCHMARK_PAGE). It defaults to nothing, which detects the target's current state from the benchmark workflows so re-scaffolding preserves an opt-in; a fresh package has none, so the default is opt-out. When disabled, none of the benchmark files are written and the docs emit no Benchmarks page. Pass benchmarks = true to opt in; update detects and preserves the state.

downgrade_compat controls the opt-in downgrade-compat CI job in .github/workflows/test.yaml (the julia-downgrade-compat reusable, which resolves the oldest compatible dep versions). A package pinned to a Julia floor — or one depending on an unregistered, [sources]-pinned package that the downgrade resolver cannot see — can never pass that job, so it opts out. It defaults to nothing, which detects the target's current state from the committed test.yaml so a resync preserves the choice; a fresh package keeps the job (the standard). Pass downgrade_compat = false to drop it (#121); the test/downgrade julia_versions inputs are separately preserved as a package-owned with: override (see _preserve_caller_with_inputs).

The README body is package-owned, but the standard badge set is managed: a block between <!-- badges:start --> / <!-- badges:end --> markers carries the docs/CI/coverage/ quality/license badges (plus per-backend AD CI + coverage badges when ad = true), parameterised from / (no owner/repo hardcoded). The block is injected after the README's # title when the markers are absent and refreshed in place when present; nothing outside the markers is touched. A missing README is created with a title and the block.

.gitignore follows the same managed-block pattern: the standard ignore rules live between # managed:start / # managed:end markers and are (re)rendered on every scaffold/update, but anything after the end marker is a package-owned tail that is never touched — add your own ignore rules there. A pre-existing .gitignore with no markers (e.g. one written by a kit version before this behaviour existed) is treated the same way a legacy README is: the managed block is inserted at the top and the whole existing file is kept below as the tail, so nothing a package added is ever silently dropped.

docs_subdomain selects how the docs site is hosted. The default (nothing) is a GitHub project-pages deploy: docs/make.jl gets deploy_url = nothing, so DocumenterVitepress derives the VitePress base from the repo name and the site renders at epiaware.org/<Repo>.jl/ with no DNS to wire — the docs work out of the box. Pass docs_subdomain = true for the conventional <pkg>.epiaware.org, or a host string for a bespoke domain, to deploy at a custom subdomain instead; this sets deploy_url to that host and points the README docs badges at it. A custom subdomain also needs a DNS record for the host and the repo's GitHub Pages custom domain set (which writes the gh-pages CNAME); until both exist the site will not resolve, so the project-pages default is preferred unless that wiring is in place. When no explicit choice is passed, the hosting is recovered from the repo's existing docs/make.jl deploy_url, so update preserves a subdomain-hosted package (and self-heals a drifted one) without the maintainer re-supplying docs_subdomain on every sync (#123). Only a never-scaffolded target falls back to the default: the kit itself dogfoods the opt-in path, defaulting to its own DNS-wired subdomain (epiawarepackagetools.epiaware.org), so its dogfood update stays stable.

The three managed README sections (Contributing, How to cite, Code of conduct) follow the same managed-block pattern between <!-- standard-sections:start --> / <!-- standard-sections:end --> markers: appended to a freshly seeded README and refreshed in place thereafter. A marker-less README that already carries a bespoke Contributing/citation/Code-of-conduct section is left untouched — migrating it to the managed block is a deliberate per-repo wording change (#67). CITATION.cff is package-owned and write-once, seeded from // (and the DOI when known); update never rewrites it, so the real author list and DOI stand.

force = true overwrites the package-owned skeletons too, and lays every managed file down fresh regardless of any EPIAWARE_MANAGED_OVERRIDE marker (see update), so a new package always starts fully managed. target_dir must exist. Use update to re-apply only the managed files later.

Returns a (created, updated, preserved, removed, readme, license, workspace, gitignore, logo, standard_sections, citation, warnings) named tuple: destination paths newly written, managed files overwritten, package-owned files left in place, retired managed paths deleted (RETIRED_PATHS, #185), the README badge action (:created, :injected, :refreshed, or :skipped), the LICENSE action, the root [workspace] stanza action (:injected, :preserved, or :skipped), the .gitignore managed-block action (:created, :injected, or :refreshed), the README logo-title action (:injected, :preserved, or :skipped when no logo file exists yet), the managed standard-sections action (:refreshed, :injected, or :skipped), the CITATION.cff action (:created, :preserved, or :skipped), and non-fatal warnings raised while applying (a Vector{String}).

source
EpiAwarePackageTools.scaffold_generate Function
julia
scaffold_generate(
    target_dir::AbstractString,
    package::AbstractString;
    authors,
    uuid,
    ad,
    benchmarks,
    kwargs...
) -> @NamedTuple{created::Vector{String}, updated::Vector{String}, preserved::Vector{String}, removed::Vector{String}, readme::Symbol, license::Symbol, workspace::Symbol, gitignore::Symbol, logo::Symbol, standard_sections::Symbol, citation::Symbol, org_branding::Symbol, warnings::Vector{String}}
julia
scaffold_generate(target_dir, package; authors = String[], uuid = <fresh>,
    ad = true, benchmarks = false, kwargs...)

Generate a fresh package at target_dir and adopt the standard tooling.

Creates the target directory if needed, writes a minimal package skeleton (a Project.toml naming package with a fresh UUID, and a src/<package>.jl module stub), then runs scaffold over it so the new package starts fully managed. Unlike scaffold — which adopts the tooling into an existing package — scaffold_generate also lays down the package's own Project.toml and source module, so it works from an empty (or non-existent) directory.

  • package — the package name (no .jl suffix).

  • authors — author entries (a Vector{String}); written to the new Project.toml and used for / substitution.

  • uuid — the package UUID; a fresh uuid4() by default.

  • ad — forwarded to scaffold: true (default) scaffolds the AD infra, false opts out. See scaffold for the full AD-opt-in behaviour.

  • benchmarks — forwarded to scaffold: opt into the benchmark CI + suite + docs page. A fresh package has no benchmark workflows to detect, so this defaults to false (opt-out); pass benchmarks = true to enable.

Remaining keyword arguments (org, repo, reviewer, year, license, ...) are forwarded to scaffold_inputs; e.g. license = "Apache-2.0" writes the Apache licence. Returns the scaffold manifest.

source
EpiAwarePackageTools.scaffold_inputs Function
julia
scaffold_inputs(
    target_dir::AbstractString;
    package,
    authors,
    holder,
    org,
    repo,
    reviewer,
    year,
    license,
    docs_subdomain,
    doi,
    zenodo_badge,
    docs_timeout
) -> NamedTuple{(:PACKAGE, :UUID, :ADFIXTURES_UUID, :AUTHORS, :HOLDER, :ORG, :REPO, :REVIEWER, :YEAR, :LICENSE, :DOCS_DEPLOY_URL, :DOCS_URL, :DOCS_TIMEOUT_WITH, :DOI, :ZENODO_BADGE, :TUTORIALS_SUBDIR, :AD_BUILD_COUNT, :AD_CODECOV_FLAGS, :AD_BACKENDS_JSON, :AD_COV_TABLE, :AD_BACKEND_PACKAGES, :AD_BACKEND_ENTRIES, :AD_SCENARIO_TESTITEMS, :CODEOWNERS_LINE, :DEPENDABOT_REVIEWERS, :ASSIGNEE_DEFAULT, :KIT_DEP_LINE, :KIT_SOURCE_LINE, :SYNC_INSTALL, :JULIAFORMATTER_VERSION, :JULIA_TEST_VERSIONS, :LOGO_INITIAL), <:Tuple{Union{Nothing, SubString{String}}, Union{Nothing, SubString{String}}, String, Union{Nothing, String}, Union{Nothing, String}, String, Union{Nothing, String}, String, String, Union{SubString{String}, String}, String, Union{Nothing, String}, String, Union{Nothing, String}, Union{Nothing, String}, String, String, String, String, Union{Nothing, String}, String, String, String, String, String, Union{Nothing, String}, Vararg{String, 6}}}
julia
scaffold_inputs(target_dir; package = nothing, authors = nothing,
    holder = nothing, org = "EpiAware", repo = nothing,
    reviewer = nothing, year = <current year>,
    license = nothing) -> NamedTuple

Resolve the placeholder substitution values for scaffold / update.

Every value defaults from the target Project.toml (or a sensible org default) and is overridable by keyword, so no person, org, or repository name is baked into a template:

  • package — the package name (); default the Project.toml name. The package UUID () is read from Project.toml uuid.

  • authors; default the joined Project.toml authors.

  • holder — copyright holder (); default authors.

  • org — GitHub org (); default "EpiAware".

  • repoowner/name slug (); default "{org}/{package}.jl".

  • reviewer — the GitHub handle () that drives every place a real reviewer/code-owner is needed: the .github/CODEOWNERS rule (* @), the Dependabot reviewers, the version-bump assignee, and the Claude bot's actor gate. A username or org/team slug — GitHub cannot assign a bare org. When omitted (nothing), no owner is written (CODEOWNERS ships a commented placeholder, Dependabot gets no reviewers) so a bare org is never hardcoded.

  • year — copyright year (); default the current year.

  • license — the SPDX licence identifier (one of MIT, Apache-2.0) selecting which LICENSE text scaffold writes and which README badge is rendered. Default nothing, in which case the licence already committed to the repo (its README badge, else its Project.toml license field) is recovered and kept (#235), falling back to "MIT" for a never-configured target — so a bare update/template-sync does not flip a non-MIT adopter's badge to MIT. The LICENSE text itself is written once and never overwritten by update so a deliberate licence is never reverted.

  • doi / zenodo_badge — an optional Zenodo DOI and badge id; when both are given a DOI badge is added to the README "License & DOI" cell (mirroring CensoredDistributions.jl). Both default to nothing, in which case any DOI badge already committed to the README is recovered and preserved (#161), so a bare update/template-sync keeps an adopter's DOI instead of stripping it. Passing either explicitly supplies or overrides the DOI on demand.

  • docs_timeout — an optional docs-build job timeout in minutes for the managed document.yaml Documenter caller. Default nothing, which renders no with: block so the reusable documentation.yml's own default (45 min) applies; pass a positive integer to cap a slow docs build. A package-owned with: block hand-added to document.yaml is preserved across update() (see _preserve_caller_with_inputs), so a set timeout survives a resync.

Returns a NamedTuple of placeholder => value pairs (plus LICENSE, the resolved SPDX identifier).

source
EpiAwarePackageTools.scaffold_update Function
julia
scaffold_update

Transitional alias for update (#294): update was called scaffold_update until this rename, kept reachable under its old name so a caller already qualifying it (EpiAwarePackageTools.scaffold_update, or an explicit using EpiAwarePackageTools: scaffold_update) keeps working across the rename. scaffold_update is public, like update itself — neither is exported, so there is no Main-binding collision either name could cause (see update's own docstring for why that used to matter and no longer does). New code should call update; this alias is removed in a future cleanup once adopters have moved off it.

source
EpiAwarePackageTools.setup_checklist Function
julia
setup_checklist(; ...)
setup_checklist(
    target_dir::AbstractString;
    package,
    repo,
    org,
    io
)
julia
setup_checklist(target_dir = "."; package = nothing, repo = nothing,
    org = "EpiAware", io = stdout)

Print the manual setup steps left after scaffold/scaffold_generate.

scaffold writes every file-based standard, but a handful of one-off steps need a human with dashboard access and no file-writer can do them for us: enabling Codecov and adding its CODECOV_TOKEN secret, wiring a docs custom domain (when one was chosen), enabling GitHub Pages, protecting main, and running the first Julia General Registry registration (via the managed Register.yml workflow — see its docstring in scaffold). This prints that checklist, followed by a ready-to-paste tracking-issue body.

package/repo/org resolve exactly as in scaffold_inputs (defaulting from target_dir's Project.toml), so the checklist reads naturally for the target package with no arguments in the common case (setup_checklist() from the package root).

This prints only: it never shells out to the gh CLI or calls the GitHub API, so it has no extra dependency and works offline. The suggested issue body is meant to be copied into a new issue by hand, or piped straight through, e.g. gh issue create --body-file -, if the gh CLI happens to be installed — setup_checklist itself makes no such assumption.

Returns nothing; everything is written to io.

Example

julia
setup_checklist()
source
EpiAwarePackageTools.test_aqua Function
julia
test_aqua(
    mod::Module;
    ambiguities,
    unbound_args,
    undefined_exports,
    project_extras,
    stale_deps,
    deps_compat,
    undocumented_names,
    piracies
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_aqua(mod; kwargs...)

Run the standard Aqua.jl quality suite over mod.

Wraps the individual Aqua.test_* checks (unbound args, undefined exports, project extras, stale deps, deps compat, undocumented names, piracies, ambiguities) in one @testset. Keyword arguments forward to each check that accepts them, so a package can relax a single check without re-listing the rest (e.g. test_aqua(MyPkg; ambiguities = false) to skip the ambiguity check).

stale_deps also accepts a NamedTuple of keywords forwarded to Aqua.test_stale_deps (e.g. stale_deps = (; ignore = [:LinearAlgebra])), so a package that deliberately keeps a dependency ahead of using it (#217) can allow just that one rather than disabling the whole check with false.

Aqua must be a dependency of the calling test environment.

source
EpiAwarePackageTools.test_docstring_format Function
julia
test_docstring_format(
    mod::Module;
    exported_only_examples,
    require_field_docs,
    require_arg_sections,
    require_examples,
    crossref_ignore
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_docstring_format(mod; exported_only_examples = true,
    require_field_docs = true, crossref_ignore = ())

Check the docstrings of every exported and public symbol in mod against the EpiAware docstring conventions.

For each documented symbol with a meaningful docstring the checks are:

  • structs document each field name somewhere in the docstring (when require_field_docs);

  • functions with positional arguments include an # Arguments section, and functions with keyword arguments include a # Keyword Arguments section (both skipped when require_arg_sections = false, for a package whose API docs are reference-style rather than sectioned);

  • exported (and public) functions include an @example block (skipped when exported_only_examples is false, which requires examples of every function instead; set require_examples = false to drop the @example requirement entirely, e.g. for a tooling package whose helpers need external fixtures to exemplify);

  • the docstring carries either a TYPEDSIGNATURES directive or the symbol's own name (i.e. a signature is shown);

  • [name](@ref) cross-references resolve to another exported/public symbol; names in crossref_ignore (a tuple of Symbols for upstream names a package legitimately links to, e.g. :pdf, :cdf) are allowed.

Symbols without a docstring are skipped here (leave existence to Aqua's undocumented_names check). The cross-reference check warns rather than fails, matching the original package-level check.

source
EpiAwarePackageTools.test_doctest Function
julia
test_doctest(
    mod::Module
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_doctest(mod)

Run Documenter's doctest over mod.

A thin wrapper that runs the package doctests in one @testset. Documenter must be a dependency of the calling test environment.

Documenter.doctest evaluates each docs page's @meta CurrentModule = <mod> block against Main. Under TestItemRunner the @testitem body runs in a sandbox module, so Main has no binding for the package and every @meta block fails with UndefVarError: <mod> not defined in Main. To make the standard @meta CurrentModule idiom work under TestItemRunner, the module is bound into Main (under its own name) before doctesting, so the @meta blocks resolve.

source
EpiAwarePackageTools.test_explicit_imports Function
julia
test_explicit_imports(
    mod::Module;
    ignore,
    implicit_ignore
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_explicit_imports(mod; ignore = (), implicit_ignore = ignore)

Run the ExplicitImports.jl conformance checks over mod.

Asserts there are no stale explicit imports, no implicit imports, that every explicit import is public in its source module, and that imports come from their owning module.

  • ignore — a tuple of Symbols for unavoidable non-public explicit imports (e.g. an upstream internal used by an extension); forwarded to check_all_explicit_imports_are_public.

  • implicit_ignore — a tuple of names that are legitimately implicit and must not fail check_no_implicit_imports; defaults to ignore. The common case is a @reexport using SomePkg, which makes the bare module name SomePkg an implicit import that no amount of explicit listing removes — pass implicit_ignore = (:SomePkg,) so a reexporting package conforms.

Package extensions are handled automatically: an extension imports its parent's (and its trigger's) internals by design, and ExplicitImports walks an extension only when it is loaded, so the verdict used to flip with extension-load order (#189). The names a loaded extension imports are folded into every check's ignore here, so the verdict is independent of whether extensions are loaded and adopters no longer need to enumerate their extensions' import lists by hand.

ExplicitImports must be a dependency of the calling test environment.

source
EpiAwarePackageTools.test_ext_ambiguities Function
julia
test_ext_ambiguities(
    mod::Module,
    extname::Symbol;
    prefixes,
    expect_phantoms,
    broken
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_ext_ambiguities(mod, extname; prefixes = (string(nameof(mod)),),
    expect_phantoms = false, broken = false)

Assert the loaded extension extname of mod introduces no method ambiguity on the package's own surface.

Aqua.test_ambiguities runs in a subprocess with no extensions loaded, so it never sees an extension's method table; this check loads in-process and filters to ambiguities mod or its extension owns (see on_surface_ambiguities). The caller is responsible for import-ing the extension's trigger package(s) before calling so the extension is loaded.

  • prefixes is the set of allowed surface module-name prefixes (see on_surface_ambiguities).

  • expect_phantoms = true additionally asserts the raw count is positive, proving third-party phantom pairs exist and the on-surface filter is doing real work (use for an extension pulling in e.g. a Symbolics integration).

  • broken = true records the no-ambiguity assertion as @test_broken, for quarantining a known, issue-tracked extension-only ambiguity without silencing it; the test flips green when the bug is fixed.

source
EpiAwarePackageTools.test_formatting Function
julia
test_formatting(
    dirs;
    style,
    verbose,
    env
) -> Union{Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_formatting(dirs; style = "sciml", verbose = true)
test_formatting(mod; ...)

Check that the given source trees are JuliaFormatter-clean.

dirs is a collection of directory paths; non-existent entries are skipped, and each existing directory is checked without modification. Passing a Module defaults to checking the src, test, docs, and benchmark directories of the package that owns mod. style selects the JuliaFormatter style (the EpiAware standard is "sciml"); the .JuliaFormatter.toml at the package root still takes precedence when present.

The test passes when every directory is already formatted. JuliaFormatter must be a dependency of the calling environment; to keep its JuliaSyntax pin from clashing with JET, run this from an isolated formatter environment (see the templates/Taskfile.yml test-formatting target).

Pass env (the path to an isolated formatter project directory holding JuliaFormatter) to run the check in a subprocess via that project's runtests.jl, exactly as test_jet isolates JET. The test then passes when the subprocess exits zero, and JuliaFormatter need not be a dependency of the calling environment — the recommended layout when the test items share an environment with JET. style/verbose/dirs are ignored in env mode (the isolated runtests.jl owns that configuration).

source
EpiAwarePackageTools.test_import_centralisation Function
julia
test_import_centralisation(
    mod::Module
) -> Union{Nothing, Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_import_centralisation(mod::Module)

Assert every genuine using/import in mod's package sits in the top-level module file, not scattered across included source files (kit issue #105).

Walks every .jl file under mod's package src/ directory (as located via pathof(mod)) and parses it looking for a using/import that shares the file's own top-level scope — exactly the scope an included file's statements run in once spliced into the parent module. The main module file itself is exempt (that is precisely where imports should live). A nested module/baremodule block defined inside an included file (e.g. a Benchmarks- or DocsBuild-style helper submodule) starts its own fresh scope, so its own top-level using/import is exempt too.

Lazy, call-time dependency loads (_require_pkg(...), Base.require(...) inside a function body) are ordinary function calls, not using/import syntax — and Julia disallows using/import inside a function entirely — so they never trigger this check.

source
EpiAwarePackageTools.test_jet Function
julia
test_jet(
    mod::Module;
    target_modules,
    env,
    skip_experimental,
    report_filter
) -> Union{Nothing, Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_jet(mod; target_modules = (mod,), env = nothing,
    skip_experimental = true, report_filter = nothing)

Run JET over mod.

JET is run in an isolated environment to keep its JuliaSyntax / dependency pins from clashing with the rest of the test environment. Pass env as the path to a project directory holding JET plus the package; that project's runtests.jl is run in a subprocess and the test passes if it exits zero. When env is nothing JET is loaded into the current environment and run directly (simpler, but only safe when JET coexists with the test deps).

report_filter is an optional predicate report -> Bool: when supplied, JET is run via report_package and the test asserts that no report for which the predicate returns true survives (a report is kept when the predicate returns true). This lets a package suppress known false positives without silencing the whole check. For a DynamicPPL @model package, pass dynamicppl_model_filter, which drops the macro's spurious ~/:= reports. When report_filter is nothing (default), JET runs via test_package and fails on any report. report_filter is ignored in env mode (the isolated runtests.jl owns that configuration).

By default JET is skipped on experimental / pre-release Julia (and when JULIA_CI_EXPERIMENTAL=true), where JET often lags the compiler.

source
EpiAwarePackageTools.test_linting Function
julia
test_linting(
    mod::Module;
    kwargs...
) -> Union{Nothing, Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_linting(mod; kwargs...)

Run JET static analysis (code linting) over mod.

An alias for test_jet under the "linting" name used by the standard package test layout; all keyword arguments forward unchanged. Prefer running JET in an isolated environment via env = joinpath(@__DIR__, "jet") to keep JET's dependency pins from clashing with the rest of the test environment.

source
EpiAwarePackageTools.test_partial_backend Function
julia
test_partial_backend(
    reg,
    name::AbstractString;
    rtol,
    atol,
    scenario_kwargs
)
julia
test_partial_backend(reg, name; rtol = 5e-2, atol = 1e-6)

Test a partially-supported backend by running every scenario through [check_broken].

Each scenario the backend supports passes; the rest are marked @test_broken. Use this for a backend that cannot run the full test_differentiation sweep without crashing.

source
EpiAwarePackageTools.test_readme_sections Function
julia
test_readme_sections(
    path::AbstractString;
    required,
    order
) -> Union{Nothing, Test.FallbackTestSet, Test.DefaultTestSet}
julia
test_readme_sections(path; required = STANDARD_README_SECTIONS, order = true)

Assert the README at path carries the standard EpiAware section structure.

path is a README file or the directory containing a README.md. The check reads the ##-level (and deeper) headings, skipping the H1 title and any heading inside a fenced code block, then asserts each entry of required is present and (when order = true) that the present sections appear as an ordered subsequence of the headings.

Ordering is a subsequence, not an exact sequence: a package-owned section may sit anywhere, including one whose heading also matches a required group (a ## License above the managed standard-sections block does not stand in for the managed ## How to cite below it, #236). When the README carries the managed markers, the sections inside the block are additionally required to be all present and in the order the kit renders them (MANAGED_README_SECTIONS) — the block's internal order is the only section order the kit itself guarantees, since it appends the block to a README whose own sections it does not move.

required is a vector of heading groups; each group is a tuple of accepted heading texts matched case-insensitively as a substring, so a package may title the section to taste (e.g. ("Getting started", "Usage")). The default is the standard structure (STANDARD_README_SECTIONS): a Why/Overview section, a Getting started / Usage section, a Documentation section, a Contributing section, and a Citing / License section. A package overrides or extends the list via its qa_config.jl (pass its own required).

The H1 title and the managed badge block are checked here too: the README must open with a single # title and contain the badge markers the scaffolder manages (see scaffold).

Keyword Arguments

  • required: the ordered heading groups to require; default the standard set.

  • order: when true, also assert the present sections are in order.

julia
test_readme_sections(pkgdir(MyPackage))
# extend the standard set with a package-specific section:
test_readme_sections(pkgdir(MyPackage);
    required = vcat(EpiAwarePackageTools.STANDARD_README_SECTIONS,
        [("Benchmarks",)]))
source
EpiAwarePackageTools.test_working_backend Function
julia
test_working_backend(
    reg,
    name::AbstractString;
    rtol,
    atol,
    scenario_intact,
    scenario_kwargs
)
julia
test_working_backend(reg, name; rtol = 5e-2, atol = 1e-6,
    scenario_intact = false)

Hard-test a working backend on the scenarios it supports.

Looks up the backend named name in reg, runs DifferentiationInterfaceTest.test_differentiation (correctness only) over the scenarios not listed as globally or per-backend broken and not in the backend's skip set, then runs the broken scenarios through [check_broken] so they record as @test_broken.

scenario_intact is forwarded to test_differentiation; it defaults to false because a scenario carrying a Missing-bearing context trips DIT's default post-run equality check (comparing a missing-containing vector with == errors in a boolean context), while the gradients themselves stay correct.

scenario_kwargs is a NamedTuple of extra keyword arguments forwarded to the registry's scenarios call, e.g. a package's own scenario-group selector (scenario_kwargs = (; category = :latent)).

DifferentiationInterface and DifferentiationInterfaceTest must be loaded.

source
EpiAwarePackageTools.update Function
julia
update(
    target_dir::AbstractString;
    ad,
    benchmarks,
    downgrade_compat,
    kwargs...
) -> @NamedTuple{created::Vector{String}, updated::Vector{String}, preserved::Vector{String}, removed::Vector{String}, readme::Symbol, license::Symbol, workspace::Symbol, gitignore::Symbol, logo::Symbol, standard_sections::Symbol, citation::Symbol, org_branding::Symbol, warnings::Vector{String}}
julia
update(target_dir; ad = true, benchmarks = nothing,
    downgrade_compat = nothing, kwargs...)

Re-apply only the managed standard files to an already-adopted package and report the drift.

update is public, not exported (#294): call it qualified (EpiAwarePackageTools.update(...)) or with an explicit using EpiAwarePackageTools: update. This used to be a bare update until #173 found that an exported generic verb collides with a package's own same-named export — using EpiAwarePackageTools; using ComposedDistributions left update unbound in Main, breaking Documenter @ref resolution — which #178 fixed with a hard rename to scaffold_update. public closes that collision a different way: a public-not-exported name is never brought into scope by a bare using, so it cannot fight another package's export regardless of what either package calls its own verb. That makes the short, generic name safe again, so #294 renamed it back. The old name is kept reachable as scaffold_update, a transitional alias, so an existing qualified caller is unaffected by the rename itself.

This is the entry point the scheduled template-sync workflow calls: it rewrites every managed standard file (root config, CI caller workflows, dependabot, and the test-infra drivers) from the bundled templates, leaving all package-owned files (unit tests, qa_config.jl, AD scenarios, benchmarks.jl, and LICENSE) untouched. In particular LICENSE is never rewritten, so a package that deliberately switches licence is not silently reverted. The workflow opens a PR when the result differs from what is committed. Placeholder inputs are resolved exactly as in scaffold; pass the same overrides to keep substitution stable across a sync.

ad must match the value the package was scaffolded with (default true): with ad = false the managed AD files (ad.yaml, test/ad/setup.jl, test/ad/runtests.jl) are not managed and the no-AD variants of Taskfile.yml and codecov.yml are re-applied instead.

benchmarks controls the opt-in benchmark CI + suite. It defaults to nothing, which detects the package's current state from the managed benchmark workflows (benchmark.yaml / benchmark-history.yaml) so a resync preserves an adopter's benchmarks rather than stripping them — the scheduled template-sync bakes the adopted value into its own update call, but a repo scaffolded before this flag re-passes nothing, so detection is what keeps that first sync idempotent. Pass benchmarks = true/false to force enable/disable.

downgrade_compat controls the opt-in downgrade-compat CI job the same way: it defaults to nothing, detecting the job's presence in the committed test.yaml so a resync preserves a package's decision to drop it (#121) rather than reintroducing a job it deliberately removed. Pass downgrade_compat = true/false to force keep/drop.

The README's managed badge block is also refreshed: update injects it when the <!-- badges:start --> / <!-- badges:end --> markers are absent and re-renders it from the current placeholders when present, so a package gets and keeps the standard badges automatically without its README body being touched.

The managed .gitignore block is handled the same way: refreshed between its markers (or migrated in place if a pre-existing file has none yet), with any package-owned tail after the block left untouched.

The README logo title (see scaffold) is also (re)checked: once a package has a docs/src/assets/logo.svg, the tag is added to the title if missing.

Every managed file written from a template has a package-owned opt-out (#224). Putting the marker EPIAWARE_MANAGED_OVERRIDE in a comment in the committed file tells update() to preserve it (leaving it in preserved) instead of resyncing it, so a package keeps its own version of that file; remove the marker to hand management back to the kit. scaffold/scaffold_generate (force = true) ignore the marker and lay the managed file down fresh, so a new package always starts managed. Use the marker sparingly: an overridden file no longer tracks the standard, which is the whole point of the kit.

Three limits on the marker, all deliberate:

  • It covers whole files emitted from a template, not the marker-delimited regions the kit injects into otherwise package-owned files. The .gitignore managed block, the README badge and standard-sections blocks, and Project.toml's [workspace] stanza are refreshed on every sync regardless of any EPIAWARE_MANAGED_OVERRIDE; a package customises those by editing outside their markers, which is what the markers are for.

  • It opts a file out of resyncing, not out of retirement: a marked file whose path the kit has retired (RETIRED_PATHS, below) is still deleted, since a retired path is infrastructure the kit no longer supports at all.

  • The marker must appear in a comment, so the two managed JSON files (docs/package.json, .secrets.baseline) cannot carry it — JSON has no comment syntax — and cannot be overridden this way. The match is case-sensitive: EPIAWARE_MANAGED_OVERRIDE, in capitals, or it does nothing.

The AD-harness driver test/ad/setup.jl is where this began (#162): the generic driver assumes the package's ADFixtures registry satisfies the current ADRegistry contract (its scenarios accepts a category keyword), so a package whose registry predates that contract would MethodError on every AD test if the driver were overwritten. That file still honours its original marker EPIAWARE_AD_SETUP_OWNED as well as the generic one; either preserves it. When the committed driver has diverged from the managed one but carries no marker — a strong signal it was customised and the marker just never got added — update() still overwrites it (managed files always resync) but records a message in warnings (and emits @warn) rather than clobbering it silently. That divergence warning is deliberately scoped to test/ad/setup.jl, the one file whose clobber is silently fatal: divergence from a fresh render is the normal state of any managed file on an adopter simply running an older kit version, so a generic divergence check would fire on every sync and mean nothing. Mark a file you own instead.

Managed files the kit has retired (RETIRED_PATHS) are deleted, so a sync converges on the current standard instead of leaving dead infra behind (#185).

Returns a (created, updated, preserved, removed, readme, license, workspace, gitignore, logo, warnings) named tuple: managed files newly added, managed files rewritten, preserved files, retired paths deleted, the README badge action, the LICENSE action (:skipped on update), the root [workspace] stanza action, the .gitignore managed-block action, the README logo-title action, and non-fatal warnings raised while applying (a Vector{String}).

source