A feature subgraph is the closed reactive subgraph
rooted at one or more outputs. It is the unit of validation. This
vignette covers how slicing works, how to author a
features.yml manifest, and the schema of the doc stub the
package emits per feature.
Default slicing rule
Without a manifest, the package emits one feature per
top-level output and per top-level side-effecting observer. The
output output$km_plot produces a feature named
km_plot; an assigned download handler
download_csv <- downloadHandler(...) produces a feature
named download_csv.
Each feature’s subgraph is the transitive upstream closure of its root, terminating at:
- Input nodes (graph leaves)
- Module instance nodes (opaque per the module-contract design)
- Top-level reactiveValues entries
Module-internal observers and reactives reachable only via a module instance node are not part of the parent’s feature subgraph. They belong to the module subgraph.
The default rule guarantees there are no unclaimed outputs — every behavior is captured. The cost is that grouping is per-output, not per-user-feature.
Module subgraphs
Each Shiny module definition produces a module subgraph in addition to any feature subgraphs. The module subgraph is rooted at:
- The module’s outputs (assigned in the body of
moduleServer()) - The module’s returned reactives (the value returned by the server function)
Module subgraphs are validated standalone. The doc stub for a module
mirrors the feature stub but is keyed under modules: in the
manifest.
A module subgraph’s contract — its inputs, outputs, returned
reactives — is auto-extracted and shown in the doc stub for the
developer to confirm. Confirmation is implicit: the module appears in
the manifest with a complete intended_use.
A module_instance node referenced by a feature, but with
no corresponding moduleServer() definition reachable in the
enumerated source, emits SVT-W104 (“orphan module
instance”) — likely a missing source or a misnamed call.
The features.yml manifest
A developer-authored declaration of feature groupings and per-feature
metadata. Lives at the app root (configurable via the
manifest argument to svt_validate()).
Schema
features:
- name: km_plot
intended_use: |
Generate Kaplan–Meier survival curves for the trial primary
endpoint cohort, stratified by treatment arm.
risk_classification: high # high | medium | low | not_required
rationale: ~ # required if risk_classification = not_required
roots:
- output: km_plot
- output: km_summary_table
- observer: km_export
package_categories:
survival: method
dplyr: framework
logger: utility
reviewers:
- role: developer
name: ~
signed_at: ~
- role: validation_engineer
name: ~
signed_at: ~
modules:
- name: filter_panel
intended_use: |
Provide reusable subject filters across analyses.
risk_classification: mediumField semantics
-
name— the stable identifier; used in artifact filenames. -
intended_use— free text; the validation surface declaration. Required. -
risk_classification— one ofhigh,medium,low,not_required. Required. -
rationale— required ifrisk_classification = not_required, optional otherwise. Encodes the “decision not to validate is part of validation” principle. -
roots— list of{output: <name>}or{observer: <name>}entries. At least one required. For module-namespaced roots, use{output: <namespace>/<name>}form. -
package_categories— optional declaration overriding doc-stub category fields (utility / framework / method). -
reviewers— placeholder structure. Names and timestamps filled by humans during sign-off.
Generating a starter manifest
graph <- svt_build_graph(svt_parse("path/to/app"))
svt_manifest_template(graph, out_path = "features.yml")This produces one entry per default-sliced feature root with empty
fields (~) for the developer to fill in.
Validating a manifest
issues <- svt_manifest_validate("features.yml", graph)
issues
#> # A tibble: 1 × 3
#> code name message
#> <chr> <chr> <chr>
#> SVT-W101 km_plot Manifest root output: km_plt not found in graphAn empty tibble means the manifest is ready to use.
The checks (per validate call):
- SVT-W101 — Manifest references unknown root.
- SVT-W102 — Two features claim the same root.
-
SVT-W105 —
risk_classification = not_requiredwithoutrationale. - fatal — name collision across features+modules.
By default (lenient = FALSE), any of these aborts the
slice. With lenient = TRUE they become warnings and slicing
proceeds best-effort.
Unclaimed-output completeness check
After slicing, the package walks the global graph for every top-level
output and observer. Any that is not the root of any manifest-declared
feature emits SVT-W103 (“unclaimed output”), returned
by svt_unclaimed().
This is the validation-completeness gate. An app with zero SVT-W103 warnings has every user-visible behavior claimed by some feature.
The default slicing rule (one feature per output) prevents SVT-W103 from firing in the default case — every output gets an auto-generated feature stub. The warning becomes meaningful only when a user-authored manifest is present and explicit.
svt_unclaimed(features)
#> # A tibble: 1 × 5
#> code file line col message
#> <chr> <chr> <int> <int> <chr>
#> SVT-W103 server.R 48 3 Unclaimed output: output:debug_textDoc stub schema
For each feature and each module, the package emits a markdown file
at <out_dir>/<name>.md. Section order:
# Feature: km_plot
## Intended use
[from manifest, or "(not declared - fill before validation)"]
## Risk classification
[from manifest, or "(not declared)"]
## Rationale
[from manifest, optional]
## Reactive subgraph
[link to <name>.html]
## Module contract # module-only
[inputs / outputs / returned reactives]
## Functions called
[auto-filled from inventory; one row per pkg::fn]
## Packages used
[auto-filled; one row per package]
## Warnings
[auto-filled; one row per warning code triggered in this subgraph]
## Reviewers
- Developer: __________________ Date: __________
- Validation Engineer: __________________ Date: __________The auto-filled sections are regenerated on every run. Human-authored sections (intended use, rationale, reviewer signatures) are preserved across regenerations by reading the existing file and merging.
Regeneration semantics
On regeneration:
- Subgraph HTML widgets are overwritten.
- Function and package inventories are overwritten.
- Warning lists are overwritten.
- Doc stub markdown is merged: human-authored sections preserved, auto-filled sections refreshed.
Auto-filled sections are: Intended use, Risk classification, Rationale, Reactive subgraph, Module contract (modules only), Functions called, Packages used, Warnings. Everything else (notably Reviewers) is preserved verbatim if it exists on disk.
This is the artifact-level analogue of “pull the latest, but keep my local edits”. A developer can sign the Reviewers section once; the signature survives subsequent regenerations until the developer removes it.
Selecting a subset of features
svt_validate() accepts features and
modules character vectors to limit the artifacts produced —
useful in CI for re-rendering only what changed.
svt_validate(
app_path = "path/to/app",
features = c("km_plot", "km_summary_table"),
modules = "filter_panel"
)Slicing still happens for the whole app (so unclaimed-output detection remains correct), but only the named features and modules get rendered.