It fixes what a teaching session on 0.4.2 found and what Ethos, the
research platform built on surveyframe, reported from its own use, and adds
references carried inside the .sframe file. It went out on GitHub on 19
October 2026 and reaches CRAN with the fixes since. SurveyBuilder no longer
changes content its user left alone, so an amended instrument saved from it
can be written again, and every plan result can be serialised by a host.
write_sframe(new_instrument = TRUE) needs a new title or version on an
instrument read with read_sframe(). It accepted the title and version of
the file read, so 2 different files could claim to be one instrument. Set
meta$version (or meta$title) before writing. SurveyBuilder applies the
same rule when it saves changed content of a file it opened.
An instrument unchanged since it was read writes without the flag, and the
error says so: Ethos, or any caller writing an unchanged file, drops
new_instrument = TRUE.amend_sframe() starts the log from the file as read. It took the
starting fingerprint from whatever previous held, so a file read, edited
in memory and then amended got a log beginning at a state no file held,
and the edit went undisclosed. previous must now match the file it was
read from, or its last recorded amendment. Amend from the instrument as
read, and put every change in instrument. write_sframe() also checks
that the first new entry starts from the content read.verify = FALSE is marked unverified, but one
amendment wrote it back as a verified file with the hand edit and a clean
log. amend_sframe() now refuses a previous that failed its integrity
check, and write_sframe() refuses any entry appended to the log of an
unverified read. Restore the original file, or write the content as a new
instrument with new_instrument = TRUE.Value (the label), Code, Frequency
and Percent. Code reading Value for the stored code should read Code.
Rows follow the choice set's declared order, and a declared choice nobody
picked appears with a count of 0.single_choice or multiple_choice column move from
the mean and SD table to a table of counts and percentages.jsonlite::toJSON(). The classed
report behind a descriptives, missing-data, quality, reliability, EFA or
item-diagnostics result, and a fitted LDA or STM topic model, now sit in an
attribute for the current R session, which toJSON() leaves out. Code
reading result$report_obj or result$fit$model should stop doing so:
extract_quotes() finds the model itself, and the charts
run_analysis_plan(plots = TRUE) draws are unchanged. The KMO and parallel-analysis parts of an EFA readiness result
are now plain lists holding the same numbers. A host serialising results
failed on all of these, 20 blocks across the demos. Charts drawn with
plots = TRUE stay on the result as ggplot objects.render is written as {}, an empty object, by
write_sframe() and SurveyBuilder. It was written as [], which a host
reading the JSON took for a list. A file written with [] still reads and
verifies, and an amendment log recorded with [] still matches its
content, so such a file takes further amendments as before.An instrument can now carry the works it draws on, and every report lists them once, in a References section at the end, in a standard bibliography style.
render_report() printed [@key] raw and left out the
instrument's references and any passed in references. It now writes
each citation as (Author, Year) and ends with an APA 7 References list of
articles, books, chapters, reports, web pages and software. An entry of
another type shows a visible note, and a csl other than APA 7 gets a
message that this engine formats APA 7 only (D8, CX-10).sf_reference() creates a reference as a CSL-JSON record, the format
Zotero, Mendeley and pandoc share. read_bibtex() reads BibTeX, as
exported by Google Scholar, Zotero, Mendeley and most journal pages, using
base R alone. add_references() attaches references to an
instrument, and sf_instrument(components = ) takes them too.
sframe_references() lists them, and sframe_bibliography()
writes the CSL-JSON a citation processor reads.cited_by field.
validate_sframe() gains a references check that every target exists
and every key is unique. A work in another language can carry language, which keeps a style
from title-casing its journal name.render_report() cites in text and lists each work once, at the end.
Each result used to carry its own reference list, so the same works
repeated through the report. Results now cite their methods in text, for
example "(Cohen, 1988)", and the codebook names the source of
each item and scale. A written interpretation can cite a work as [@key].
The list at the end is formatted by pandoc's citation processor in APA 7,
bundled with the package, or in any style given as csl = . The new
references = argument adds works a host cites in the write-up.
render_results() lists its references once at the end in the same way.sf_reference(), read_bibtex(), where a work is cited, and
what render_report() does with the list.read_bibtex() reads entries written with parentheses. BibTeX allows
@article(key, ...) as well as @article{key, ...}, and a file may mix the
2. A closing bracket inside a braced or quoted value ended a parenthesised
entry early.read_bibtex() refuses a key used by more than 1 entry. The later
entry replaced the earlier without a word. It is now an error naming every
repeated key, as add_references() does for an id the instrument holds.read_sframe(text = ) reads a payload held in memory, such as JSON a
host keeps in a database, with no temporary file. verify = FALSE
reads content whose hash is missing or does not match, with a warning of
class sframe_unverified_warning, and marks the instrument with the
attribute sframe_verified = FALSE, which print() shows. A host can then
keep every field where it used to rebuild the instrument by hand. Such an
instrument is written only as a new instrument, or when its amendment log
names exactly its content.inst/schema/sframe_schema.json is now a copy of sframe-schema's
instrument profile, kept identical by a test. The 2 had drifted: the
bundled copy left sframe_format optional and lacked references, so a
file could pass one and fail the other. It now requires sframe_format,
as write_sframe() writes it. read_sframe() still reads older files
without the field. Every bundled .sframe file and a builder save
validate against it.references is a new optional top-level field. It is written only when an
instrument has at least one, so an instrument with no references keeps the
file hash it had, the rule designs and amendments already follow.amend_sframe() records leaves references out, so a
reference can be added to a published instrument directly.inst/schema/sframe_schema.json documents the field. Its description of
the amendment content hash was also wrong: it named only the items,
choices, scales, branching, checks, plan, models and designs, while the
hash has always covered every field except hash and amendments,
meta, render and version included. Corrected.inst/csl/apa.csl, the APA 7 style from the Zotero Style Repository, is
bundled under CC BY-SA 3.0. See inst/COPYRIGHTS.error opens with the block id and the method, as in
"RQ3 (McNemar test): ". Code that matched the start of an error message
needs to allow for it.Multinomial logistic regression has a full table. It gives b, SE, z,
p, the odds ratio and its 95% interval for each outcome category against
a named reference category, and the result text names the reference.
render_result() also returns a result's syntax where it has one. The
logistic demo results are regenerated (CX-14).
Model outlines give the notes for their own model type.
model_report_template() gave every model the same notes, asking an EFA
for structural paths and a PLS-SEM model for fit indices. EFA, CFA, CB-SEM
and PLS-SEM now each get their own list. PLS-SEM follows Hair, Hult,
Ringle, Sarstedt, Danks and Ray (2021), doi:10.1007/978-3-030-80519-7, and
checks each construct by its mode (reflective, formative or single item)
with the thresholds the book gives, then the structural model. The seminr
script's assessment section follows the same checks.
The seminr script tests every declared indirect effect.
seminr_syntax() writes one specific_effect_significance() call per
sf_indirect(), as the lavaan syntax already defined each one.
EFA reads factor item columns by their labels. efa_report() and
efa_solution() read item columns with the same rule as every other
numeric method, so a factor holding 10 to 50 counts as those values, where
its level positions had been used (CX-06).
Regression syntax codes predictors as the analysis does. The code
analysis_syntax() writes for linear, binary, ordinal and multinomial
logistic regression and ANCOVA codes each predictor the way the package
does: a number when every value reads as a number, a category otherwise.
"1" to "5" stored as text had entered the code as a factor, so it fitted a
different model. The ordinal, multinomial and ANCOVA runners now code
predictors and covariates by the same rule, so a text covariate that is
not a number enters as a category, where it had become missing.
analysis_syntax() now covers ordinal and multinomial logistic
regression.
Choice descriptives honour split_by and weights. A descriptives
block counted choice items raw and pooled whatever its grouping and
weights. Counts and percentages now come per group, in the order the
choice set declares, and weighted when a weight column is named (D5,
CX-07).
One check for every weight column. Frequency, cross-tabulation and descriptives blocks stop with an error naming the weight column when a weight is negative, infinite or not a number, or when the weights sum to zero. A row with a missing weight is dropped, and the result says how many were (D6, CX-09).
Kendall's tau reports N, with no degrees of freedom. cor.test()
gives Kendall no df, yet the line read tau(58) and the table carried a
df column. It now reads tau = 0.39, 95% CI [...], p < .001, N = 60.
Pearson and Spearman keep r(df) and r_s(df) (D9, CX-17).
The paired t test names the higher measure. Its APA line and prompt
end with ", higher in
Named weights in sensitivity_analysis() match criteria by name.
Named weights are matched to the criteria in any order, as the ranking
blocks already do, and a name that is not a criterion or a criterion with
no weight is an error. Unnamed weights keep the order of the criteria.
Logistic syntax codes the outcome as the analysis does. The code
analysis_syntax() writes for a binary logistic regression recodes the
outcome to 0 and 1 the way the runner does before glm(), so a text
outcome runs and the coefficients match the result.
ANCOVA syntax writes every covariate. analysis_syntax() for an
ANCOVA wrote only the first declared covariate, so its F differed from
the result. It now writes each one (CX-02).
Two-group effect sizes keep their direction. Cohen's d for the
independent t test, and r for Mann-Whitney and the Wilcoxon signed-rank
test, were magnitudes, and their bootstrap intervals were intervals of a
magnitude, so they could never include zero. Each is now signed, first
group minus second in the declared order, as the paired d_z already was.
Size labels read the magnitude, and the APA line and prompt name the higher
group. cohens_d_ci() returns the signed interval. The two_group,
paired and small_sample demo results are regenerated: their r values
can print with a minus sign, and the small_sample r interval now starts at
0.12.
A two-group test takes its groups in the declared order. The
independent t test and Mann-Whitney ordered the 2 groups by first
appearance in the data, so re-sorting the responses flipped the sign of t
and z. The order is now the grouping item's choice set as the instrument
lists it, then factor levels, then sorted values. The code
analysis_syntax() writes names the same order.
A statistic is never replaced by a choice label. A result table read
every text cell through the instrument's labels, so a statistic equal to a
choice code took that code's label: the factor_structure demo's "Suggested
factors" read "Disagree" in place of 2. Choice codes are now relabelled only
in a column of response values, and never in a Statistic and Value table.
Item and scale ids still read as their labels everywhere.
A descriptives plan table has reader headings: Variable, N, Valid N,
Missing, Mean, SD, the confidence interval's bounds and the rest, as the
other plan tables have. It showed valid_n, missing_n and sd. The
group column is left out when there is no split, and the weighted mean
when there are no weights. descriptives_report() keeps its own column
names, and the demo reference results keep theirs.
Report sentences agree in number: "computed for 1 variable" in place of "1 variable(s)".
A frequency block gives one frequency table per variable. It reads the
variable role, or variables, and either may name more than one item. With
more than one, $tables holds each variable's own result and $table
stacks them with a Variable column, and the chart shows one panel per
variable. The variables role was ignored before, so a block written that
way reported "Frequency distribution for NA (N = 0)". SurveyBuilder and
SurveyStudio both let the role take more than one variable.
A frequency table labels each code from the item's own choice set. A code that means different things in 2 choice sets (1 for Male in one, 1 for North in another) used to print as the bare code.
Text codes such as "01" keep their leading zero when responses are read.
read_responses() converted every choice column whose codes looked like
numbers, so "01" and "02" became 1 and 2, and a frequency table showed the
declared options First and Second with a count of 0 beside unlabelled rows
1 and 2. Only codes that survive a numeric round trip, such as 1 and 2, are
now read as numbers. The rest stay as written.
Result text names variables and groups by their labels. The tables
and charts used the instrument's labels, while the text beside them said
attendee_type, "higher in in_person" and "reference category keynote".
A variable now appears as its label in double quotes, since a label is
often a question, with any double quotes inside it turned to single ones.
A group or category appears as its choice label. An interaction term in a
table, such as attendee_role:format, reads as its 2 labels joined by
a times sign. A runner called outside run_analysis_plan() keeps the
ids (CX-20).
A block that cannot run says which block, what is wrong and what to do. McNemar, Friedman, ANCOVA, Fisher's exact test, partial correlation, the ordinal, multinomial and Firth logistic models and moderation said only "X failed.", and EFA readiness passed on "'x' is empty". Each now names the data problem, suggests a remedy and keeps R's own message. An infinite value in a variable is named as the cause, where R's message did not say it (CX-13).
read_responses() reads semicolon and tab files. It detects a comma,
semicolon or tab separator from the header line, and a new delim
argument overrides the detection. A semicolon file is read as Excel writes
it in a comma decimal locale, so 3,5 in a numeric column reads as 3.5,
and a message says so. A byte order mark no longer joins the first column
name.
A descriptives block counts single_choice items and the option columns
of a multiple_choice item, giving each category's count and percentage,
and keeps the mean and SD for numeric items. A nominal code has no
meaningful mean, and a mean gender of 1.59 was being reported. The result
carries the counts in $categorical, the APA line names the items counted,
and sframe_result_supplement() returns the counts so both report engines
show them beside the numeric table. render_results() now shows that
second table for every result that has one.
The printed quality report has a Duplicates section, so a reader can see why a respondent counted as flagged. It also says when the check needs a respondent id to run.
A scale shorter than straightline_min_items is still left unchecked for
straight-lining by default, since identical answers to 3 items are common
among attentive respondents. The printed report now names the threshold and
the setting that would check the scale, and the analysis-plan quality table
names the threshold too. The argument's help says how to check 3-item
scales, in a call or in a plan.
An efa_solution block that names a missing item says which. Its
items and variables roles went unchecked, so a block naming an item the
instrument or the responses lack failed with "EFA solution requires at least
two available item columns." and never named the item. It now fails the
standard check, as other methods do, naming the item.
A statistic that cannot exceed 1 has no leading zero. Result text and
prompts write r, r_s, tau, phi, Cramer's V, R squared, eta squared and
their intervals as APA 7 does, r(58) = .53, 95% CI [.33, .69], as p
values already were. d, t, z, means and other values keep the zero. Done
once for every analysis, and the demo results are regenerated (D9, CX-24).
Every report engine prints a table's cells the same way. The built-in
engine and the Quarto template rounded every number to 2 decimals, so a p
of .004 showed as 0. A p column now shows 3 decimals with no leading zero,
or <.001, a column that cannot exceed 1 drops its leading zero, other
numbers show 2 decimals, and a missing value is a blank cell, in
render_report(), render_results() and render_result() alike. The new
sframe_apa_cells() does it (CX-15).
Result text in an HTML report sets its symbols as APA 7 does. t, F,
p, r, d, N and the other Latin symbols are italic, r_s and d_z print
with a subscript, tau and phi as Greek letters and chi-square as its
symbol. The text was plain in all 3 HTML renderers (CX-24).
Table headings set symbols as APA 7 does. A p column was headed
"P", t "T" and df "Df", and render_results() printed raw names such
as missing_pct. Symbols are now italic, r_s has its subscript, a
_pct column (a proportion) is headed "(proportion)" and drops its
leading zero, and other names read as words, in both report engines (CX-24).
Result text counts with real plurals. "7 power-iteration step(s)", "5 choice column(s)" and the decision methods' notes and errors now read "7 power-iteration steps" and "1 respondent". The demo results are regenerated (CX-24).
A PDF report matches the HTML report from the same call.
render_report(format = "pdf") dropped title, authors, references,
csl and figures on the way to the HTML report it prints. It now passes
all 5 (CX-04).
Tables and figures are numbered, titled and described. The built-in
report, render_results() and render_result() number every table and
figure in reading order as APA 7 does, with the number in bold and an
italic title below it. A result's table and chart are titled by the block
and its research question instead of "Results table", each chart sits in a
figure with a caption, and base graphics charts carry real alt text where
every one read "distribution" (CX-22).
The built-in report prints each research question's hypotheses and decision rule before its result. H0 and H1 print where written, then the planned decision rule, then the result, then the interpretation, labelled "Interpretation (written after the results)". The decision rule printed only beside a written interpretation, and an interpretation stood in for an empty decision rule. The 2 fixed sentences earlier versions of SurveyBuilder and SurveyStudio saved as hypotheses, and an interpretation copied from the decision rule, are not printed.
render_results() and render_result() print the same declaration.
H0, H1 and the decision rule print above the result, and the
interpretation below it under the same label. render_results() showed
the decision rule as the interpretation when none was written, and a
result from run_analysis_plan() carried its decision rule as its
interpretation. A result's interpretation is now only what was written.
The Quarto report prints the same declaration as the built-in one. It
printed the block's interpretation as an "Interpretation prompt" before
the data existed, and no hypotheses. render_report() now computes each
block's declaration and interpretation with the built-in engine's helpers
and hands them to the template, and the interpretations help for
render_report() and render_results() says what prints where.
A Quarto report records the surveyframe that rendered it. Its
Reproducibility section gives the version, and the commit where the
install carries one (RemoteSha, written by remotes and pak). Quarto
renders in its own R process, which loads the installed package, so this
names the code that produced the numbers.
The tests render Quarto reports with the build under test. A test
setup file installs the source once per run and puts it first on
R_LIBS, so every child R process, Quarto included, loads it. The
earlier render_report() shadow never ran under devtools::test(), so
Quarto tests had rendered with the installed copy.
The Quarto build guard fails when a git source tree gives no commit. It skipped the commit comparison on an empty commit, so a failed lookup let a report from a stale install pass on its version alone.
The tests have a fast tier and a slow tier. In a source tree, the 18
tests that render through Quarto or drive Chrome run only when
SURVEYFRAME_SLOW_TESTS=true, and the source install they need is made
only then. The skip message names the variable. The fast tier renders
every report with the built-in engine from the source under test. Under
R CMD check nothing changes.
render_report() shows the report it writes. In an interactive
session the new open = TRUE default opens it in the viewer pane of
RStudio, Positron or VS Code, or in the web browser elsewhere, and
open = FALSE keeps it closed. A knitr or Quarto render opens nothing. The
completion message gives the full path, where it gave the path as written,
often a bare file name. A line says when Quarto starts, since it works in
silence for up to a minute, and figure_dir = reports where the chart
files went.
New render_result() renders one analysis result as the report does:
the formatted table with its caption and note, any second table, the APA
line, and the chart the report picks for that method, with the
instrument's labels, as an HTML fragment or with the chart written as PNG
or SVG. It uses the report's own code, so a host showing results one at a
time and the report agree. A host had to keep its own map from method to
chart, which drifted with each release.
Figures at the size, resolution and format a journal asks for.
render_report(figures = list(width =, height =, dpi =, format =)) applies
to every figure in both engines, and defaults to the surveyframe.figures
option, so a host can set it once. figure_dir = also writes each plan
chart as its own file, PNG, TIFF, JPEG, PDF or SVG. New
sframe_save_figures() does the same for the results of
run_analysis_plan(plots = TRUE). The report fixed its own figure size,
and the built-in engine saved charts at 110 dpi.
SVG uses svglite where it is installed, now in Suggests, and R's own
Cairo device otherwise. Where neither is available the report draws PNG,
and a request to write SVG files says to install svglite.
codebook_report(format = "md") gives Markdown. The codebook carries
itself as Markdown text in markdown, a heading and a pipe table for the
items, choice sets, scales, plan and models, and print() writes it. Save
it with writeLines(cb$markdown, "codebook.md"). The format argument
changed nothing before, so a host received the printed text table.
render_report() takes title and authors, which default to the
instrument's title and authors. Every Quarto report was titled "Survey
Research Report" whatever the study. The date under the title is labelled
"Rendered" in place of Quarto's "Published", since a rendered report
has not been published. The built-in HTML engine shows the authors too.
null and
alternative. validate_sframe() accepted any shape there, and a report
has nothing to print from a hypothesis that is no text.items ran on nothing and reported
"Frequency distribution for NA (N = 0)" with an empty table.
validate_sframe() now says which roles the method reads and which the
block names, and run_analysis_plan() gives such a block an error in
place of a result. The roles a method reads are found from the same
resolver the analysis uses, so the 2 stay in step. partial_correlation
and mediation also accept every role alias their runners read.run_analysis_plan() now names a
variable the responses lack, saying whether it is an unknown name, an item
with no column or a scale with no scored column, and a test that fails
says which variable holds no numbers, or that too few responses had a
value, or else passes on R's own reason.sf_choices() and by validate_sframe(). Responses to a set whose codes
are all numbers are read as numbers, so the codes 1 and 1.0, or 2 and 02,
became one option, and sframe_export_labelled() then failed on duplicate
labels.amend_sframe()'s help says how to change the wording on the welcome,
consent and thank-you screens. That wording is inside both hashes, so on
a published instrument it goes through a pipeline-tier amendment with
reason_code = "other" and then write_sframe(). A host editing the
JSON directly leaves a file read_sframe() refuses. The help also lists
references among the fields the content hash leaves out.a and b with labels A
and B read back from sframe_export_labelled() as A = 0 and B = 0. A
.dta now holds such codes as 1, 2, 3 in the set's order, each with its
label. A .sav keeps the text codes, which SPSS labels directly.sframe_export_labelled() failed
on matrix row columns such as session__Opening keynote and on long MCDA
pair names, and sent long labels to Stata uncut. Such names are now made
valid for the format, with a number where 2 would match, and labels over
the format's limit are cut. One message lists every change as old -> new,
and the table is attached to the returned path as attr(path, "renamed")
(D10, CX-18).render$thankyou$message sits below it in every case except a failed send,
so a debrief or contact details reach a Google Sheets respondent too. Before,
the package's own text replaced the message there. The default message is now
"Thank you for participating." because the old default said "recorded" over
a survey with no collector. The branded_survey demo no longer claims its
feedback was recorded. A preview export keeps the study's message too, so
a researcher sees the text respondents will read, under the status line
that says nothing was sent.survey_module_server() said "Your response has been recorded." and
render_survey() said "Submitted successfully" whether or not anything
stored the answers. A module with no on_submit, or render_survey() with
save_responses = "none", stores nothing. A status line now says what
happened: recorded to the CSV, submitted to on_submit, or not saved. The
study's message sits below it, and the module's default message is
"Thank you for participating."window.sframeSendResponse(row), returning a Promise, takes over the send.
Resolving to true or {confirmed: true} shows the response as recorded,
in place of the note that the page cannot confirm storage, which every
respondent saw, even on a host that confirms.render$thankyou$show_restart = FALSE hides "Submit another response".render$save_draft = TRUE keeps the answers in the browser as
the respondent goes, so a closed tab can be resumed from the welcome screen.
The draft is cleared once the response is submitted, and kept when delivery
fails. SurveyBuilder's exported survey carries the same template.export_static_survey(preview = TRUE) removed the endpoint and the redirect, but a page defining
window.sframeSendResponse() still received the preview's answers while the
screen said nothing was sent. A preview flag in the page now blocks every
delivery path, the host's included.render_result() draws the chart in the palette you ask for. It
used the chart attached to the result whatever palette said. A palette
given explicitly now rebuilds the chart with it. Left out, the attached
chart is used as before (CX-19).sat was
titled "Observed against fitted values for Sat" while its table read
"Satisfaction". This covers the correlation, regression, group comparison,
partial correlation and moderation charts, in render_report() and in
SurveyStudio.The Plan editor has fields for the hypotheses. Methods that test a hypothesis show optional "Null hypothesis (H0)" and "Alternative hypothesis (H1)" fields, with a method-appropriate example as placeholder text. Only what you write is saved, and the plan card shows it. The builder saved 2 fixed sentences, "No effect or association is present." and "An effect or association is present.", for every such block, and those load back as empty.
The decision rule and the interpretation are separate text boxes.
One box filled both fields. The Plan editor's box is labelled "Decision
rule (declared now)" and saves the decision rule only. The report outline
shows the rule read-only, with an "Interpretation" box that saves the
interpretation only once results exist. An interpretation identical to
the rule counts as unwritten, and a file holding only interpretation
still opens with it as the rule.
Changed content of an opened file saves only as a new instrument. The builder checked a file's hash when it opened and then forgot it, so an edited instrument saved as verified, with no amendment. It now keeps the title, version and content it opened. Saving changed content of a file that had a hash asks for a new version, offering the next one, or cancels. A new instrument needs a new title or version, and the amendment log stays with the original. A file opened without a matching hash asks on every save.
A choice set can be declared ordered. sf_choices(ordered = TRUE)
marks options that run in a meaningful order, such as a frequency or an
education level, and SurveyBuilder has the same switch in the choice set
dialog and beside an item's choice set. A single_choice item on an
ordered set is tagged ORD, ordinal, in SurveyBuilder and SurveyStudio, so
it reaches the methods for ordered categories. Every single_choice item
was tagged NOM before, the known limitation 0.4.3 shipped with. The field
is written only when TRUE, so existing files keep their hashes.
A plan block saved with the outcome role, which R accepts beside
dependent, opens with that variable selected. Regression blocks opened
with the first listed variable in its place. The builder reads every role
alias R reads, and saves under its own role names.
A mediation block written in R with x, mediator and y opens in the
plan editor with those variables selected. The editor replaced x and y
with the first listed variable and left Save disabled. R now holds one role
alias table by method, inlined into the builder, and a test fails if the two
differ. A required role with no variable chosen shows "Choose a variable"
and keeps Save disabled, where it showed the first variable.
The sidebar shows the instrument's title on first load. A title set by
launch_builder_demo() read "Untitled Survey" until the first edit.
The Outcome and other role dropdowns stay inside the Add or Edit Analysis Plan dialog when an item label is long. The role grid columns can now shrink, and each select fills its column.
SurveyBuilder keeps the amendment log. Opening an amended .sframe
file in the builder and saving it dropped its amendments, designs and
sframe_format fields, so a disclosed revision vanished silently. The
builder now carries every field it leaves unedited through to the next
save.
Every file saved from SurveyBuilder carries sframe_format, as
write_sframe() writes it. sframe-schema's instrument profile requires
the field, so each new file saved from the builder failed that schema.
Save settings writes only the fields you changed. It wrote every
General, Welcome and Thank You field from its input, so an absent author
list became an empty string and absent screens took their default
wording. On an amended instrument that was a content change nobody had
made, and write_sframe() refused the file. Opening a file also added
empty screen objects to it. An emptied author list or description is now
saved as absent, as R writes it.
Settings saved on an instrument with an empty render are kept. R
writes an empty render as a JSON array, and the builder held it as one,
so a theme or welcome screen saved there vanished from the saved file.
All 19 demos with an empty render were affected.
Open asks before replacing unsaved changes, and opens the file chooser at once when there are none. It replaced them silently. Opening a file also closes the inspector, which kept showing an item from the survey being replaced, and the status reads "Opened" until the next save. It read "Saved just now", which inside a host such as Ethos reads as saved in the host.
The question editing area is a named, focusable region, so a keyboard user can reach and scroll it. At narrow widths it was a scroll region reachable by pointer alone.
SurveyBuilder's Survey settings dialog scrolls. A tab taller than the dialog ran past its bottom edge, with its last controls out of reach. The dialog body now shrinks to fit and scrolls.
Each screen draws when it is opened. Studio kept 28 outputs on every screen computing from start-up, because its screens are hidden with CSS and Shiny was not told when one became visible. The screen switch now tells Shiny, so only the open screen's outputs are computed. A browser test opens every screen and checks that it draws.
The Analysis Plan screen has fields for the hypotheses. Methods that test a hypothesis show optional "Null hypothesis (H0)" and "Alternative hypothesis (H1)" fields, with a method-appropriate example as placeholder text. Only what you write is saved. Studio saved 2 fixed sentences, "No effect or association is present." and "An effect or association is present.", for every such block, and those load back as empty.
The decision rule is no longer copied into the interpretation. The plan editor box is labelled "Decision rule (declared now)" and saves the decision rule only. Saving an edited block keeps the interpretation it already had, and an interpretation identical to the decision rule counts as unwritten.
Saving an opened file untouched keeps its content hash. Studio rebuilt the instrument metadata without the file's creation time, so the saved draft carried the time of the save. Any file opened and saved unchanged then failed its hash check and Studio asked to save it as a new instrument. The creation time now carries over from the file.
SurveyStudio has a References screen. It lists the works the open
instrument cites, adds more from pasted BibTeX with where the instrument
cites each one, and removes them. Changes apply to the open instrument at
once, need no amendment, and travel in the exported .sframe file.
launch_studio(screen = "references") opens on it.
SurveyStudio keeps an instrument's references. Its rebuild of the open instrument had no place for them, so they were dropped as soon as a file opened, and lost from anything exported.
The dashboard's filters are named by their questions and offer the
choice labels. They showed the item id, such as visit_type, and the
stored codes. Each label is now tied to its control.
Run results on the Analysis Plan screen take the full width. Each result card sat inside the middle third of the screen, about 230 px wide, so a result table printed "120" one digit per line and cut off its p column. The cards now sit in a full-width row below the 3 columns on the Run preview tab.
The Quality Dashboard says when the duplicate check needs an id column, where it
printed "NA duplicate response row(s) detected". It now uses a
respondent_id column for the check by default.
The Saved analysis plans table keeps short ids such as "RQ1" on one line and scrolls sideways on a narrow screen.
A result card is headed "RQ 4" when its block id only repeats the number. It read "RQ 4: RQ4". A block id that says something more, such as "H2a", still shows beside the number.
branded = TRUE still replaces the whole block. instrument_revision is
rebuilt on the updated first_survey, and the learn-by-example screenshots
are recaptured.sframe_demo_branding() prints each setting as a respondent meets it,
with a pointer to preview_demo(). It printed a raw nested list, which
read as a failed demo. It returns a list of class sframe_branding, and
sf_instrument() stores it as a plain list.sframe_demo("instrument_revision") takes a further amendment. Its
amendment log recorded hashes from code that predates the settled content
hash, so amend_sframe() on it failed with "The amendment log is out of
sequence at entry 2", and so did write_sframe() after any change. The
demo is regenerated, its content unchanged, and a test now amends and
writes every shipped demo.preview_demo() opens a branded demo as a respondent sees it in one line,
in the browser or on SurveyStudio's preview screen. The start-up note now
shows it.first_survey, instrument_revision, verification, branching,
conversational_branching and multi_response.A defect-fix release. An external review of 0.4.1 found defects that silently alter or lose data, and this release corrects them. Collection defects come first, because an answer recorded wrongly is lost for good. This section grows as each group of fixes lands.
A Shiny-collected response now carries a respondent_id, as the first
column, holding a generated identifier such as RK3P8QX2A. The static
survey has written one since the first release, so the two collection
routes produced different column sets for the same instrument, leaving the
duplicate check and a retry to work from whatever they could find.
If you are collecting to a CSV written by 0.4.1 or earlier, that file
predates the column: render_survey() keeps appending to it and says once
that the response went in unidentified. Collect into a new file to record an
identifier for every response. Anything reading these files by column
position needs updating to read by name.
survey_module_server() returns a different response shape. The
response is now the collection row, as a list: response_id,
started_at, submitted_at, then one element per response column, named
as read_responses() expects. A multiple-choice question becomes one
element per option holding "1" or "0", and a matrix, ranking or
decision question becomes one element per row, option, pair or criterion.
Values are character. An item hidden by branching, or left unanswered, is
NA. The module previously returned one raw input value per item, and
lacked controls for matrix, ranking, rating and decision questions.
Untouched sliders, rankings and dates are now unanswered in
render_survey() and the survey module. A slider counts once the
respondent moves it, and a ranking once it is reordered or confirmed with
"Keep this order". Previously a slider stored its starting position, a
ranking stored the declared order, and a date question in the module
stored today's date, for anyone who moved past them.
render_survey(save_responses = "csv") refuses a response file written
for different questions, when the app starts and on each submission.
Collect a changed instrument into a new file. A file with the same columns
in another order is still accepted, and rows are aligned by column name.
Google Sheets collection now needs the Sheets advanced service. The
generated collector stores an answer through the Sheets API's RAW option,
which is the documented way to store a value without the spreadsheet parsing
it. Regenerate and redeploy the collector with export_google_sheet(), and
in the Apps Script editor add Services > Google Sheets API with the
identifier Sheets. A collector lacking it refuses each response and
replies with an error, in place of storing an answer it would have to alter.
Code that reads the sheet directly, bypassing read_sheet_responses(),
should expect text cells where numbers appeared before.
Scale scores and reliability can change. Re-run analyses of scales
that reverse-code items, share items with other scales, or had an absent
item column. Reverse coding now applies within the scale that declares it,
a scale with an absent item column counts that item as unanswered, and
report figures now show the scores score_scales() computes.
Some instruments that validated before are now rejected. An item and a
scale sharing an ID, an ID equal to a response column or to respondent_id,
response_id, started_at or submitted_at, reverse coding for an item
outside the scale declaring it, repeated scale items, a min_valid outside
1 to the number of items, and weights that are zero, negative or infinite
are all reported by validate_sframe(), and sf_scale() refuses the scale
parameters directly. Rename or correct the declaration.
A reverse-coded item needs declared response bounds: a numeric choice
set, slider_min and slider_max, or a rating maximum. Scoring reports an
error for a reversed item that has none.
A factor column is scored through its labels. A factor whose labels are text is an error. Convert it to numeric codes first.
Some statistics change. Re-run analyses using Cochran's Q with missing answers, ANCOVA, partial correlation, a Firth likelihood ratio, PLS-SEM constructs with non-consecutive indicators, or a Mann-Whitney or Wilcoxon signed-rank test. The rank tests now use the normal approximation without continuity correction for z, p, r and its interval alike, so their p values move slightly, and z is now signed by the direction of the difference.
Repeated-measures ANOVA now uses complete respondents. A respondent
missing any repeated measure is excluded as a unit before fitting the
classical balanced model. Results report the retained n and
n_excluded_incomplete. This replaces a singular fit on incomplete rows.
ANCOVA tables hold adjusted tests, each term tested after all others,
with columns effect, df, sum_sq, mean_sq, F and p.
sample_size_plan() now calculates power for t tests, ANOVA and, with
the new f2, regression. The new d and f arguments give the expected
effect, and a medium effect is assumed, with a warning, when they are
left out. Estimates now depend on alpha and power.
run_analysis_plan() refuses repeated block IDs and gains strict.
Its results carry a status attribute counting failed blocks, so check
it, or set strict = TRUE, before treating a returned object as success.
Bootstrap intervals can be withheld. bootstrap_ci(), cohens_d_ci(),
cramers_v_ci() and eta_sq_ci() return NA bounds with a reason
attribute when fewer than 90% of resamples give a value or all give the
same value, and every result records its resample counts.
Decision rankings can change. Re-run TOPSIS, VIKOR, MOORA, SMART, WASPAS, PROMETHEE and ELECTRE analyses whose weights or criterion types were named in a different order from the performance matrix, AHP and ANP analyses given a data frame, and aggregations of judgement matrices named in different orders. Weights, criterion types and judgement matrices are now matched by criterion name.
Some decision inputs that ran before are now refused: weights naming
different criteria from the matrix, supplied AHP matrices that are not
positive, unit-diagonal and reciprocal, WASPAS values of zero or below,
infinite values, a VIKOR v or WASPAS lambda outside 0 to 1, ELECTRE
cutoffs outside 0 to 1, and PROMETHEE thresholds that are negative or out
of order. ANP refuses a reducible network, and DEMATEL a matrix whose total
relation does not exist.
ELECTRE's kernel follows Roy's definition, so it can hold more alternatives than before, and is reported as undefined when the outranking relation has a cycle.
sensitivity_analysis() results gain n_perturbations, n_effective
and n_failed, and stable is FALSE when no perturbation moved the
weights. A requested sensitivity run that could not be made is recorded
in the result's sensitivity_error.
write_sframe() refuses an undisclosed revision. An instrument read
with read_sframe() and then changed must record the change with
amend_sframe() before it is written, and its amendment log must stay
complete and in order. To publish changed content as a separate instrument,
clear its amendment log and pass new_instrument = TRUE. SurveyStudio
offers the same choice when exporting an edited file.
amend_sframe() sets the tier from what changed. An amendment that
changes the analysis plan, a model or a conjoint design is design tier and
needs a deviation_report, whatever reason_code or tier says.
.sframe files write one-member collections as arrays, as the
published instrument profile and the builder already did. Reading an older
file and writing it again gives it a new hash where it held such a
collection. Older files still read and verify as they are.
read_responses() reads a CSV file as text and converts only columns
of items with numeric responses. Identifiers such as 001 and metadata
columns now arrive as text, and numeric answers as doubles. A response file
with 2 columns of the same name is refused.
link_git_commit() gains path and returns verified, which is TRUE
only when the instrument matches that file as committed. linked alone
confirms a repository and a commit.
Some shipped demo results changed to match the corrected statistics:
likert_scale, two_group, paired, multi_group, sem_pls,
mcdm_choice and small_sample.
The Shiny survey erased every answer. In render_survey(), each answer
re-drew the survey page, and the re-drawn questions reported themselves
empty, so every answer was wiped about a second after it was given. A
submitted response came back blank. This affected every standard-mode
survey run with render_survey() in 0.4.1 and earlier. Answers now stay
put, and the page stays as it is when a question is
answered.
The Google Sheets collector could turn an answer into a formula. An
answer beginning with = was evaluated by the spreadsheet, so =1+1 was
stored as 2, and a code such as 007 lost its leading zeros. A stored
answer is now written through the Sheets API's RAW option, which stores it
as submitted. A deployment lacking that option refuses the response and says
so, in place of altering it. The same fix applies to collectors generated
from the survey builder. See the setup note above.
The exported survey said a response had been recorded when it could not
know. The page posts with no-cors, which leaves the collector's reply
unreadable, so the browser can establish that a request left and nothing
further. The thank-you screen claimed the response had been recorded, hid the
download, offered a restart that discarded the page's only copy, and could
auto-redirect away from it. It now reports that the answers were sent and
that receipt is unconfirmed, keeps the download available, withholds the
restart that would clear the response, and leaves any redirect for the
participant to choose.
A custom thank-you message no longer shows on a survey with a collector.
A message set through render$thankyou$message is fixed text, written before
anyone knew how delivery would go, so a wording such as "your response has
been recorded" put a claim on screen beside the page's own statement that
receipt was unconfirmed. The package's own status is shown instead. A custom message still
shows on a download-only survey, where nothing is being claimed about a
collector. Put debrief text, contact details or payment instructions on a
final display item or in the redirect target, where they always reach the
participant.
The exported survey changed typed numbers. Clearing a number field with
a minimum wrote the minimum in, a number outside the range was replaced
with the nearest limit, and in a points allocation 2.5 became 25. What
the respondent types is now kept, and the survey asks for a correction.
An option coded 0 was recorded as a blank in the exported survey, so a respondent choosing it on a required question was blocked from continuing, and 0 looked identical to a blank. Affected 0/1 codings and 0 to 10 scales.
A ranking could show one order and submit another in the exported survey after dragging, and an untouched ranking was submitted as if the respondent had chosen the order shown.
Shiny matrix and ranking questions showed codes in place of labels. A
five-point agreement scale appeared as 1 2 3 4 5. Shiny rankings also
stored labels, so every rank came out empty wherever labels and codes
differed.
Shiny rankings needed a mouse. Each option now has move up and move down buttons, and the new position is announced.
Shiny decision questions started with an answer selected, "Equally important" or "No influence", and points allocations started at 0, so an untouched question submitted an invented judgement. They now start empty. Every Shiny date question was also pre-filled with today's date, and now starts empty.
Appending to a Shiny response file ignored its header, so a changed instrument wrote answers under another question's heading. An existing empty file also received headerless rows.
The survey module lacked 5 item types. Matrix, rating,
ranking, pairwise comparison and criteria weight showed only a
placeholder, and a required one made the survey impossible to finish. The
module now uses the same questions as render_survey().
The survey module submitted answers the respondent had removed. A cleared answer, and an answer to a question branching later hid, were both submitted. A multiple-choice answer lost all but its first selection when the page was revisited.
A failed save in the survey module showed the thank-you screen. The
survey was marked complete before on_submit ran, and an error there
ended the session. The respondent now sees a message, stays on the page
and can try again.
Changing a reactive instrument now resets the survey module, as its help said it did, and starts every answer blank. Moving between pages scrolls the module into view, where it scrolled the whole host page.
The static survey said a response had been recorded before it knew. A
failed send was discarded and the thank-you screen appeared anyway, and a
configured redirect then carried the participant away with the answers in
nobody's hands. A failure now says so, keeps the CSV download reachable,
offers a retry, and withholds the redirect. The submission is a no-cors
POST, so the collector's reply is unreadable and acceptance cannot be
confirmed from the page. The screen claims only that the request was sent.
A completed comparison or points allocation counted as unanswered in the progress display, because progress read the question's own answer where these types store one answer per pair or per criterion. Progress and the required-question check now share one rule.
A rating left the question that depends on it hidden until another control was touched, while the required check still demanded an answer to it.
A comparison answered on a phone could show the wrong selection on a wider screen, and the reverse. Each question renders a button strip and a dropdown, shown by screen width, and each recorded the answer while leaving the other as it was. The stored judgement was always the one given.
Long rating scales now stack on a phone. An 11-point scale needed 550 pixels, so a participant on a 390-pixel screen saw part of it with both ends off screen. Below 600 pixels each point becomes a full-width row.
Choice groups, rating scales and validation errors now read correctly to a screen reader. A group carries its question as its name, each rating star reports whether it is the one chosen, and an error is announced with the control that has it. Validation and a page change move focus to the task instead of only scrolling.
A multi-select answer failed a branching rule that allowed any of its options. Selecting two options stored them together, and the static survey compared the pair as one value, so a rule showing a follow-up for either option stayed closed. Any selected option the rule allows now satisfies it, which is what the Shiny survey already did.
A second branching rule on the same question replaced the first in the Shiny survey, so a question gated on two conditions ran on one. Every rule is kept and they combine, and a question controlled by one that is itself hidden now counts as unanswered, so a stale answer behind a closed branch keeps the questions below it closed too. The static survey already worked this way.
One-question-at-a-time mode ignored branching, so a participant could be required to answer a question the rules exclude, which was then blanked on submission. Navigation follows the same visible sequence the rest of the survey uses.
A failed callback after a saved response invited a duplicate. The save
and the on_submit callback shared one error handler, so a callback
failure reported that nothing was saved and submitting again wrote the
answers a second time. The two steps are tracked separately, a retry
repeats only what failed, and the two failures now read differently.
preview = TRUE, a new export_static_survey() argument that
removes the collector and the completion redirect. If you previewed a
configured instrument in Studio on 0.4.1 or earlier, check the collecting
sheet for test rows.surveyframe: prefix: Design an instrument, Open analysis workspace,
Analyse an existing instrument, and Insert starter instrument.
SurveyBuilder was described as a Shiny app, and it is a client-side HTML
page..sframe fails to load. It used to handle a missing rstudioapi
alone. The exported launchers keep raising their errors.validation, and
ends with a commented write_sframe() line for saving it.library(surveyframe) now says where to start. In an interactive
session it prints a short note: 3 commands that load a branded demo and
show it as a respondent sees it and inside SurveyStudio, the functions to
start your own study with, citation("surveyframe"), and the website.
Scripts, Quarto renders and R CMD check stay silent, and
suppressPackageStartupMessages() removes it.score_scales() replaced the item's answers with the score, and an
analysis of that item read the score. Shared names are now rejected at
validation, and score_scales() refuses to overwrite data.reverse_items reversed it everywhere, which changed the scores and the
alpha of any other scale using that item.min_valid, so a
declared sum of 2 and 4 was shown as 3.min_valid = NULL, meaning every item, a 3-item scale with 1 absent column was scored on
2 items. The absent item now counts as unanswered, with a warning.item_report() diagnostics now match the scale's scoring. Item-rest
correlations use the reversed orientation, where a reverse-keyed item used
to come out strongly negative beside a high alpha. They use respondents who
answered every item, where a missing item counted as 0. Floor and ceiling
use the item's declared lowest and highest response, where they used the
sample's extremes, and are NA for an item that declares no bounds.sf_scale() now checks its parameters, and refuses a constructed item
passed inside a scale's items, which was silently dropped from the
instrument.Q1 and Q3 was written as the range Q1 to Q3, adding Q2.sample_size_plan() returned 64 or 50 per group for t tests and ANOVA
whatever alpha and power were requested, while printing both.render_results() table put a whole row in a single cell,
headers included. Each value now has its own cell.var_equal, and t tests and one-way ANOVA judged
significance at .05 whatever alpha the block declared. Options a method
never reads are now listed in options_ignored.term_context(window = 0) copied the match into its own context. A zero
window now gives empty context, and a negative one is refused, as is
max_matches below 1 and an n-gram size below 2.cor.test(x, y, method = "pearson")
where the wrapper call was all that showed before. Both
report engines show it, and render_report(show_code = FALSE) leaves it
out.analysis_syntax() returns that code, for one result or a whole set.
header = TRUE prepends the lines that load the instrument, read the
responses and score the scales, giving a script that runs on its own.cfa_syntax() and its neighbours.p loses its leading zero,
as p = .032, and an interval says its level, as 95% CI [0.12, 0.48].
The sentence is plain text, so italicising the symbols stays the author's
step, and ?sf_apa now says so.render_report(include_analysis = FALSE) still printed a seed beside the
instrument hash, which reads as provenance for an analysis that never ran.hash, meta and items, and
the reader refuses a file from a newer major format version.read_responses() changed values on the way in. A respondent ID of 001
became 1, a text answer of NA became missing, a matrix missing a row
column raised no warning, and a duplicated column was silently dropped.sframe_export_labelled() left matrix cells, option columns and text-held
codes unlabelled. Every response column now carries its row, option or
criterion wording, and every column of choice codes carries value labels.reverse flag,
keeping only the reversal declared on a scale. An item marked
reverse = TRUE was scored as though answered in the same direction as
the rest, moving every composite and alpha built on it.reverse_items stay
on the scale, where a rebuild used to copy them onto each item, which
changed a loaded instrument's content simply by opening it.export_google_sheet() told researchers to let anyone with the link edit
the response sheet, which exposes participant data to anyone holding the
URL. The collector writes through the sheet it is attached to, so link
sharing was always unnecessary. The help now says to keep the sheet private. If you
followed the old advice, review the sheet's sharing settings.vignette("mcdm-analysis").survey_module_ui() and survey_module_server() help is rewritten,
covering supported item types, what is submitted, a failed save, and
changing the instrument, with a complete example that stores responses.item_report() help describes item-rest correlations, where it said
item-total, and the nested result it returns, with both ways to extract it.
The sf_scale() help states the rules for min_valid, weights and
reverse_items.sample_size_plan() help separates power calculations from precision
targets and rules of thumb, and states which arguments each one uses.sensitivity_analysis(), sframe_dematel_compute() and
sframe_rated_matrix() help describe the new counts, the undefined
total relation, and how rated items pair with a weight item.amend_sframe(), link_git_commit(),
read_sframe() and write_sframe() states what the hashes establish: a
canonical content check, local and unsigned. They identify content, and
who wrote an instrument or when needs other evidence. The bundled schema
says the same.read_responses() help describes one contract for undeclared columns
and documents expansion columns and value conversion.vignette("scale-reliability-validity") said that supplying construct
scores returns the HTMT matrix. It returns the absolute inter-construct
correlations, and records htmt_method = "correlation_fallback" to say which
it computed. The Henseler heterotrait-monotrait ratio needs
items_by_construct, which records htmt_method = "henseler".
validity_report()'s own help was already accurate, and the vignette
overstated it in an unevaluated chunk. The section now names what each
argument gives, and its examples run, so a build would contradict the claim
if it drifted again.citation("surveyframe") abbreviated the author as "Sharafuddin M".
The given names Mohammed Ali were passed to person() as one string, which
R shortens to a single initial. They are now 2 given names, so the text
citation reads "Sharafuddin MA". The BibTeX entry keeps its correct form. The citation year now comes from the release date, where it
followed the date the citation was run.?sframe_plots and
?sframe_as_data_frame, and the Usage of sf_item() and
sample_size_plan() ran past the right margin in 83 places. The tables are
now lists, the Usage lines wrap, and the manual lets a line stretch
its spaces before letting a name overflow.callr, chromote, httpuv and pkgload join Suggests. They are used only
by tests that drive a survey in a real browser, which are skipped on CRAN.
sframe_demos(). Load one with sframe_demo("two_group"), or get a
Quarto notebook to edit with sframe_demo_qmd("two_group"). Together they
cover every analysis method and every question type.vignette("learn-by-example"). Pick the demo that matches the data you
have, and follow it from questionnaire to report.sframe_export_labelled() writes SPSS .sav or Stata .dta with the
question wording and response options attached, so variables arrive
labelled rather than as codes.run_analysis_plan() gains a seed argument, set by default. Bootstrap
confidence intervals and the EFA parallel analysis previously drew from the
random stream unseeded, so the same data gave a slightly different interval
on every run.
Confidence intervals will move once when you re-run an older analysis.
Test statistics and p values are unaffected. Use seed = NULL for the
previous behaviour.
render_report() now says which engine produced the file, Quarto or the
built-in writer, in a message, in an engine attribute, and in the report
itself beside the instrument hash and the seed.
%in% with more than one value never worked in an
exported survey. The question stayed hidden whatever the respondent
answered. Present since 0.3.0.sem_lavaan_syntax() produced a mediation model lavaan could not fit.
Indirect effects referred to path labels that were never written.quality_report() gains
straightline_min_items. When no scale is long enough to check, the report
now says so instead of omitting the chart silently.read.csv() no longer mangles matrix columns
whose labels contain spaces. Use check.names = FALSE.haven and V8 join Suggests, both optional.
A major release. It adds multi-criteria decision analysis (10 methods),
small-sample statistics, text and open-ended response analysis (9 methods),
and a disclosed-amendment and Git-linked provenance trail for .sframe
files, alongside 4 corrected results and 2 breaking changes. See below for
full detail on each.
surveyframe's decision-family extension links survey collection directly to 10 MCDA methods, closing the gap between MCDA computation packages, which assume a clean matrix already exists, and survey software, which has no concept of a decision method at all.
pairwise_comparison (Saaty's 1-to-9 ratio scale for AHP and
ANP, or a 0-to-4 directed influence scale for DEMATEL) and
criteria_weight (a constant-sum allocation across criteria).R/decision_data.R) turns per-respondent
answers into the matrices the methods consume: sframe_assemble_pairwise()
builds one matrix per respondent and validates every pair was answered,
sframe_aggregate_judgements() combines them (geometric mean for AHP/ANP,
which preserves reciprocity, or arithmetic mean for DEMATEL), and
sframe_rated_matrix() builds a performance matrix from ordinary matrix
items. AHP judgements are additionally screened for consistency against
Saaty's random-index table, with the CR distribution reported whether or
not a study has pre-declared a filtering threshold.sensitivity_analysis() reports how far a ranking moves under a declared
perturbation of the weights, and carries a degenerate flag so a
ranking that never separated its alternatives cannot report false
stability (see "Decision analysis: non-results now say so" below).A track of corrections for comparisons run on small samples, where the ordinary versions of these tests can flip significance on repeated draws from data whose true difference never changed.
logistf in Suggests) for
regression prone to separation at small n.assumption_report() and
sample_size_plan(), flagging when a study's sample size falls in the
range where these corrections are worth considering.vignettes/small-sample.Rmd walks through when to prefer each
correction over its conventional counterpart.A 9-method text-analysis family for open-ended survey items, from term frequency through topic modelling, sharing the same analysis-plan, role-resolution, and reporting pipeline every other method family uses.
term_freq: top terms by frequency, optionally split by a group
variable, rendered as a bar chart or a word cloud.ngram_freq: top bigrams or trigrams by frequency.term_context: a keyword-in-context concordance table (before/match/
after) for a chosen keyword.co_occurrence: pairwise within-response co-occurrence counts on the
top terms, rendered as a heatmap.co_occurrence_network: a Louvain-clustered (Blondel et al. 2008),
force-directed (Fruchterman & Reingold 1991) term co-occurrence
network; requires the optional igraph package.tidy_sentiment: positive/negative sentiment counts and proportion
positive using the bing lexicon, optionally split by a group variable,
rendered as a diverging bar chart or a positive/negative comparison
word cloud; requires the optional tidytext package.quanteda_dfm: a document-feature matrix summary (feature count,
sparsity, top features); requires the optional quanteda package.topic_model_lda: Latent Dirichlet Allocation topic modelling, top
terms per topic as a ranked table and a faceted bar chart; requires
the optional tidytext and topicmodels packages.stm_topics: structural topic modelling, the same top-terms-per-topic
output; requires the optional stm and tidytext packages.clean_text_responses()) and a 174-word
Snowball-based English stopword list, both exported so a study can
reuse or override them outside a runner.k) options
that steer their plots and models.vignettes/text-analysis.Rmd walks through cleaning, each method, and
what the family deliberately does not attempt (stemming/lemmatisation,
tf-idf, and keyness comparison are not yet implemented).write_sframe()'s SHA-256 hash proves a .sframe file is unchanged since
it was written, but gives no way to distinguish a legitimate revision
(a data-entry correction, bot-response removal, a documented model
respecification) from an undisclosed edit -- both break the hash
identically. This release adds a disclosed-revision path alongside the
existing hash check, without weakening it.
amend_sframe() compares an instrument before and after a change and
appends a structured, timestamped entry to an ordered amendment log --
never overwrites -- recording the reason (a controlled vocabulary:
data_correction, bot_removal, model_respecification,
instrument_revision, other), a free-text explanation, and which
top-level fields changed."pipeline" amendments
(data corrections, bot removal) need only a reason. "design"
amendments (anything touching the analysis plan or a model) require a
deviation_report describing what changed in the research question,
method, or model and why, matching how a formal preregistration
deviation is normally handled. signoff is never left blank -- it
records a reviewer's name or the literal "none", so an unreviewed
design change stays visible to an auditor.amendment_log() returns the full history as a data frame, one row per
disclosed amendment, exportable with write.csv()..sframe file, bypassing amend_sframe(),
still fails read_sframe()'s integrity check exactly as before. The
amendment log adds a disclosed path alongside the existing hash check.link_git_commit() records the current Git commit SHA and subject line
alongside an instrument. This ties the SHA-256 hash to a specific,
already-explained commit. It returns an informative message when Git
isn't installed or the path isn't a repository. Git is optional.inst/schema/sframe_schema.json documents the .sframe format (every
top-level field, including the new amendments log) as a standalone
JSON Schema, so a reviewer or a second tool can read and validate a
.sframe file without installing the package. .sframe was already
plain, git-diffable JSON before this release; the schema makes that
format explicit and independently checkable.vignettes/surveyframe.Rmd gains a "What the SHA-256 hash proves, and
what it does not" section, stating plainly that the hash proves file
identity, not methodological validity, and pointing to the
design-time analysis_plan binding and run_analysis_plan()'s
single-pass execution as the package's separate, complementary defence
against HARKing and p-hacking.Four defects found by independent cross-validation are fixed. Each produced normal-looking numbers with no error or warning, so re-run any results computed with an earlier version.
item_report() returned the wrong item-rest correlation. It subtracted
each item from a rowMeans() total, which leaves roughly noise carrying
the item negatively, so a highly reliable scale reported strong negative
values. On simulated data with alpha 0.947 every item came back at about
-0.46. The statistic is now the item against the sum of the other items
in its scale, and matches psych::alpha()'s item.stats$r.drop to
1e-10.aov() treated it as a continuous covariate. On a fixture where
jmv::anovaRM() gives F(2, 78) = 86.93, surveyframe reported F = 1.45,
p = 0.24. Correcting the identifier alone was not sufficient: the
corrected design produces no Error: Within stratum, so the effect is
now located by searching the strata directly.validate_sframe() rejected valid instruments. Its known-variable list
held only base item and scale ids, so an analysis plan naming an
expansion column (item__sub, item__option, item__a__vs__b,
item__crit) failed validation for variables that do exist, including
real exports from the visual builder. read_responses() already
accepted those columns. Both now derive the list from one shared helper.seminr_syntax(),
sem_lavaan_syntax(), and cfa_lavaan_syntax() never checked
model$type, and the builder offered every saved model to all 3
generators, so a covariance-based model produced PLS-SEM syntax with no
complaint. That is a runnable script estimating a model the researcher
never declared. All 3 now refuse a mismatched estimation family, and the
builder filters each model role to the types its generator can produce.Both validators previously returned two different things depending on
strict: the object itself, invisibly, when strict = TRUE, and a bare
unclassed list when strict = FALSE. Neither was a diagnostic, the
success path printed nothing at all, and the strict = FALSE return had
no methods. Both now return an sframe_validation object, and they
return it visibly, so validate_sframe(instrument) typed at the console
shows the user what it found.
valid, every problems message, and a checks
table listing all 18 instrument checks (10 for a model) whether or not
each found anything. A diagnostic that lists only failures cannot tell
a user that a check passed from one that was never reached.print(), summary() for the check roster,
as.data.frame() for one row per problem, sf_is_valid(), and
sf_problems().strict = TRUE still aborts with sframe_validation_error when
anything is wrong. That has not changed.$valid and $problems keep working, so the common reading
pattern needs no migration.strict = TRUE return as an
instrument, as in instrument <- validate_sframe(instrument). Wrap it
in as_sframe(). Passing a validation result where an instrument is
expected now raises a directed error naming as_sframe() immediately.Raised by a Journal of Statistical Software editor reviewing the code: "we would at least expect that the object is not silently returned and that the print method is adapted to allow the user to read directly the diagnostic".
The same review found that the classes carried print, summary and
format only, so user code had no route to their contents except $ on
the underlying list, which makes the internal layout part of the public
contract. Two facts made that concrete: as.data.frame() failed on all
14 result classes with "cannot coerce class ... to a data.frame", and [
dropped the class on the list-backed reports, so results[1:2] silently
degraded to a bare list and lost its print method.
as.data.frame() now works on the instrument and on every report
class, returning that object's primary table.[ keeps the class on sframe_analysis_results,
sframe_reliability_report and sframe_item_report.sf_meta(), sf_items(), sf_scales(),
sf_choice_sets(), sf_branches(), sf_checks(), sf_models() and
sf_plan(), with sf_plan<- for declaring the plan. The component
accessors return an sf_component_list named by ID, so
sf_items(instrument)[["sat_1"]] reaches one item.sf_id() and sf_label().sf_apa() and sf_flagged().as_sframe().The vignettes and the examples are rewritten to use these accessors. The registered S3 method count goes from 41 to 103.
render_survey() pipe-joined a matrix item's cells into a single column,
so a matrix question answered in the Shiny survey arrived as
mx = "4|5" where read_responses() and the whole analysis layer expect
mx__r1 and mx__r2. Data collected that way could not be read back by
the package at all, and nothing said so at collection time. Ranking and
multiple-choice items had the same shape problem.render_survey() will see its matrix, ranking, and
multi-select columns change name and layout between versions. Responses
already gathered under the old shape need re-shaping before they can be
read, and the decision item types are unaffected because they emitted the
correct columns from the start."matrix"
level, and neither surface gave matrix items one: the studio classified
them as "identifier" and the builder grouped them under "expanded",
which no role accepts. The effect was that the rated-matrix path, where
respondents rate every alternative on every criterion, could only be built
by writing R directly, even though it is one of the 3 declared ways to
supply a decision matrix. Matrix items now carry their own "matrix"
level in both surfaces.sensitivity_analysis() gains a degenerate flag for the same reason. A
ranking that never separated the alternatives cannot be changed by
perturbing a weight, so every check passed and stable came back TRUE:
the strongest robustness signal the function can give, produced by the
weakest result it can be handed. print() now leads with "No result to
test" instead of "Stable" in that case.quality_report() counted only columns matching a bare item id, and
multi-column items never post under those, so every expansion column was
invisible to the missingness check. A respondent who skipped an entire
pairwise battery was reported at 0 percent missing. Expansion columns now
count as item data, which brings matrix, ranking, multi-select, and the 2
decision item types into the missingness figures for the first time.
Reported missingness rates will change for any instrument using those
item types, because columns that were silently excluded are now counted.
Straight-lining and timing are unaffected: straight-lining runs over
declared scales, and timing is measured on the clock.cb_sem
model, so sframe_demo_data() generated PLS-SEM syntax from a
covariance-based model, and every vignette and example loading it
inherited the same mismatch. Each demo now carries a real pls_sem
model with composite constructs. The instrument hashes changed with it.sframe_decision_options() documents PROMETHEE's preference functions
and records why the default is "usual", Brans and Vincke's type I step
function, chosen over the linear function several other implementations
default to. Net flows differ between the 2 functions, and the ranking
changed in 226 of 400 randomly drawn 4-alternative by 3-criterion
matrices.This release completes the plotting, interface, statistics, and reporting work started in 0.3.3. Every analysis family now has a chart, every effect size ships with a confidence interval, reports accept written interpretations and print to PDF, both dashboards gain quality and correlation panels, date questions gain bounds, and the builder and vignettes pass a WCAG 2.2 AA accessibility audit. Hard dependencies are unchanged. naniar and pagedown join Suggests.
bootstrap_ci() (percentile
bootstrap for any statistic), cohens_d_ci(), cramers_v_ci(), and
eta_sq_ci().d_ci on the t-tests, r_ci on Mann-Whitney and
Wilcoxon, eta_ci on ANOVA and Kruskal-Wallis, ci on the correlations
(analytic Fisher z for Pearson, bootstrap for the rank methods), and
v_ci on chi-square and cross-tabulation.d = 0.62 [0.18, 1.05]. Data too small for an interval keeps the
previous string.validity_report() computes the Henseler heterotrait-monotrait ratio
when item-level data is supplied through the new items_by_construct
argument. Without it, the previous correlation-based fallback applies
and the htmt_method element records which was used.missing_data_report() runs Little's MCAR test when naniar is
installed. Without naniar the result is unchanged.reliability_report() records why omega is unavailable for a scale in
an omega_note, and the reliability chart names those scales in its
subtitle.efa_solution() adds three tidy data frames ready for plotting and
reporting: loadings_long, communalities_table, and
variance_table.render_report(format = "pdf") prints the HTML report to PDF through
pagedown, which requires a local Chrome or Chromium. HTML output is
unchanged and remains the default.interpretations argument on render_report() and
render_results(). Pass a named list keyed by analysis-plan block id to
add a written interpretation to each research question after the results
are known. The report shows it beside the pre-declared decision rule, so
the prospective plan stays visible next to the post-hoc narrative.
Interpretations are report content only and are never written into the
instrument file.run_analysis_plan(plots = TRUE) now attaches a chart to every
supported family: regression diagnostics (4 panels), EFA scree and
loadings heatmap, reliability bars, mosaic and crosstab, correlation
heatmap, quality flag rates, group-comparison boxplots, paired slope
charts, raw-variable distributions, repeated-measures profiles, a
partial-correlation residual scatter, logistic-regression odds-ratio
forest plots, a moderation interaction plot, and a mediation effect
chart. Every analysis-plan block now returns a table, a chart, or
generated syntax.plot() methods for descriptives, EFA, quality, reliability,
validity, missing-data, and analysis-results objects. plot(results)
draws every attached chart, and plot(results, which = "rq_id") returns
one.plot_palette argument on run_analysis_plan() and
render_report(): "web" for brand colour on screen, "print" for
black and white suitable for print and journal submission. SurveyStudio
exposes the choice as a Chart theme option on the Export screen.date_min and date_max bounds in sf_item(),
the SurveyBuilder, and the exported survey. The date picker enforces the
bounds and typed dates outside them show a clear message.read_responses() produces.sf_item()'s date_min and date_max accept only "YYYY-MM-DD" or a
Date object now (an ambiguous string such as "01/02/2024" used to
parse silently into a specific date depending on locale). Anything
else raises a validation error.bootstrap_ci(), cohens_d_ci(), cramers_v_ci(), and eta_sq_ci()
no longer alter the random-number seed for code that runs after a
reproducible, seeded call.This release adds an opt-in plotting layer, fixes bugs surfaced by the package's first field deployment, and redesigns the survey-taking experience. ggplot2 joins Suggests; hard dependencies are unchanged.
plots argument on run_analysis_plan() (default FALSE). When
TRUE, supported analysis blocks return a ggplot object in $plot: bar
charts for frequency and chi-square blocks, and scatter plots with a
regression overlay for correlation and regression blocks.theme_surveyframe(), a publication-oriented ggplot2 theme
with an accessible fixed-order series palette. All plots use it.$table data frame ready for
knitr::kable(); the HTML report shows these tables automatically.item__option = 1 for the top choice), so ranks are directly analysable.
Multiple-choice items likewise export one 0/1 column per option instead
of a single comma-joined column. read_responses() accepts the expanded
columns for ranking, matrix, and multiple-choice items without warnings.render_report() now attaches each analysis block's chart directly under
its result table, in both the Quarto and internal HTML report paths,
instead of tables and plots appearing in separate places.read_sheet_responses() gains a meta_cols argument for extra sheet
columns a host application appends, and SurveyStudio's dashboard now
computes completion times from imported sheet responses.sem_lavaan_syntax() turns free-text path labels into valid lavaan
parameter names (a label starting "H1:" becomes the parameter H1).seminr_syntax() output loads seminr and uses summary() accessors, so
the generated code runs as pasted.run_analysis_plan() accepts pls_sem as an alias for seminr_syntax..sframe verifies and reports its SHA-256 integrity status.This release corrects the package citation, completes the S3 method surface for the component classes, and improves the graphical tools and the HTML report. It adds no new exported functions, no new statistical methods, and no new bundled datasets.
inst/CITATION now reports the correct package title and reads the version
dynamically from the package metadata, so the citation no longer pins an old
version or an outdated title.print(), format(), and summary() methods for the component
classes sf_choices, sf_item, sf_scale, sf_branch, sf_check, and
sf_model, so each class now has a visible, documented S3 surface.lavaan is declared in Suggests. It is used only to fit the syntax produced
by cfa_syntax(). The package itself generates syntax and never requires
lavaan to be installed.export_static_survey() and export_google_sheet().render_report() renders reliably through Quarto when it is installed. A path
defect that made the Quarto render fail and fall back to the plain internal
output is fixed.This is a patch release. It fixes the static-survey to Google Sheets to R collection loop, repairs a serialisation defect, and improves the first-time user experience. There are no new exported functions, no new statistical methods, and no new bundled datasets.
export_static_survey() now renders the header logo and institution name
from render$header, so exported surveys match the Shiny renderer and
the builder preview.export_static_survey() now falls back to the instrument's
render$google_sheets_endpoint when endpoint_url is not supplied, so
a Google Sheets endpoint set in the builder is honoured on export.respondent_id, matching the Google Apps Script collector and
read_responses(). The collection round-trip now preserves the identifier.export_google_sheet() now includes matrix sub-item columns
(item_id__sub) in the Apps Script header row, so matrix answers are
stored in the Sheet.read_sheet_responses() now declares started_at as a meta column and
no longer raises a warning on every read.image/png, image/jpeg,
image/gif), so JPEG and GIF logos display correctly in the builder,
the Shiny renderer, and the static export.write_sframe() now strips list-level names from the item, choice, scale,
branching, check, and model collections before serialisation. Instruments
built with Map() or other helpers that attach element names (for example,
using item IDs as names) previously serialised those collections as
keyed JSON objects, producing a hash mismatch and an integrity error
on read_sframe(). Saved instruments now round-trip correctly
regardless of how the component lists were constructed.sframe object.
The message points the user to sf_instrument() and read_sframe()
instead of showing a raw inherits() assertion failure.reliability_report() no longer prints psych internal warnings to the
console. McDonald's omega is skipped silently for scales with fewer than
three items, where the statistic is not meaningful.run_analysis_plan() when no analysis plan is
present now describes both the programmatic route
(instrument$analysis_plan) and the visual SurveyBuilder route.surveyframe.Rmd) as an end-to-end worked
example: design the questionnaire, export it as a hosted survey with a
Google Sheets backend, collect responses, score them, run the analysis
plan, and render a report. The results section uses simulated responses so
the vignette builds offline; a single read_sheet_responses() call
connects the same workflow to live responses. The questionnaire and concept
are adopted from Sharafuddin, Madhavan, and Wangtueai (2024,
Administrative Sciences, 14(11), 273,
doi:10.3390/admsci14110273), with generic destination wording so the
example transfers to any tourism services context.sf_instrument() examples now include a complete analysis_plan block.install.packages("surveyframe"), adds a short
path for users who already have a response CSV, and points to
browseVignettes("surveyframe").The first CRAN release of the full workflow: a typed instrument object carrying the questions, the analysis plan, and the measurement model, with deployment, collection, analysis, and reporting built around it.
variables/test analysis blocks. New plans can store family, method,
roles, options, hypotheses, decision_rule,
reporting_references, status, and requires_data.descriptives_report(),
missing_data_report(), assumption_report(), posthoc_report(),
validity_report(), and sample_size_plan().run_analysis_plan() to dispatch the v0.3 method registry,
including descriptives, missing data, sparse-table tests, related-sample
tests, Kendall and partial correlations, two-way ANOVA, ANCOVA, repeated
ANOVA, ordinal and multinomial logistic regression, mediation, moderation,
and model-syntax output.sf_construct(), sf_path(),
sf_covariance(), sf_indirect(), sf_model(), validate_model(),
model_json(), add_model(), efa_solution(), efa_syntax(),
cfa_lavaan_syntax(), sem_lavaan_syntax(), seminr_syntax(), and
model_report_template(). Syntax generation does not require lavaan or
seminr.cfa_syntax() remains available as a backward-compatible wrapper around
cfa_lavaan_syntax().Added export_static_survey(). This produces a single, self-contained
HTML file that runs the survey in any modern browser without a Shiny
server or an internet connection. All thirteen item types are fully
rendered (Likert, single choice, multiple choice, matrix, numeric, text,
long text, date, slider, rating, ranking, section break, text block).
Branching logic, required-field validation, a progress bar, welcome and
thank-you pages are all handled in client-side JavaScript. On submission
the browser downloads a per-respondent CSV file. An optional
endpoint_url argument adds a parallel JSON POST to any serverless
endpoint (Google Apps Script, Netlify function, etc.).
The exported file is suitable for hosting on GitHub Pages, Netlify, or any static file server, and can also be shared directly as an e-mail attachment and opened from disk.
The SHA-256 hash written into .sframe files by write_sframe() and by
the SurveyBuilder HTML is computed using the same canonicalisation
algorithm, so instruments round-trip correctly between the browser and R.
Added launch_dashboard(). Opens a five-panel Shiny dashboard for
exploring collected response data alongside the instrument definition,
without modifying either. The panels are: Overview (response count, date
range, instrument metadata), Items (per-item bar charts, histograms, and
frequency tables), Scales (scale score distributions with mean overlay),
Quality (attention-check pass rates), and Raw data (a scrollable response
table with CSV download).
When called without arguments the dashboard loads the bundled tourism
services demo. When called with a user-supplied instrument and no
responses argument, it opens in metadata-only mode showing instrument
structure.
Added sframe_demo_data(), sframe_input_types_demo_data(),
launch_builder_demo(), launch_studio_demo(), and
launch_dashboard_demo() for CRAN-safe examples, training, and local GUI
testing.
Added a bundled input-types demo instrument and simulated response dataset for testing SurveyBuilder, SurveyStudio, the dashboard, and all supported item controls.
launch_studio() now accepts preloaded instruments, response data frames,
CSV response paths, initial screen selection, host, port, and browser
control. SurveyStudio reads these preloaded values during startup.
Added survey_module_ui() and survey_module_server(). These allow a
survey to be embedded inside a larger Shiny application as a first-class
module. survey_module_server() returns a reactive that holds NULL
until the form is submitted, then returns the response as a named list
keyed by item ID.
ui <- fluidPage(survey_module_ui("s1"))
server <- function(input, output, session) {
resp <- survey_module_server("s1", instrument = instr)
observeEvent(resp(), { saveRDS(resp(), "response.rds") })
}
An optional on_submit callback fires immediately on submission, before
any observeEvent() elsewhere in the app.
run_analysis_plan() now implements four additional tests used by the
SurveyBuilder's test dropdown:
anova_one: One-way ANOVA with eta-squared effect size. When the
result is significant and there are more than two groups, Tukey HSD
post-hoc output is included in the result object.t_test_pair: Paired-samples t-test with Cohen's d_z.wilcoxon_pair: Wilcoxon signed-rank test with r effect size.regression_logistic_binary: Binary logistic regression with
McFadden R-squared and an overall model chi-square test. The full
coefficient table is returned for interpretation.All four runners produce an APA-formatted summary string and an
interpretation prompt field to guide write-up.
write_sframe() validates the instrument and writes the validated object,
preserving meta$validated = TRUE in the saved .sframe file.
.sframe serialisation now includes a models field and continues to read
older .sframe files where models is absent.
read_responses() no longer requires display-only items such as
section_break and text_block to appear as response columns.
validate_sframe() now checks model references, analysis-plan roles,
invalid model IDs, duplicate model IDs, and model indicator/path integrity.
launch_builder(open = TRUE) opens the SurveyBuilder HTML in the system's
default browser via utils::browseURL().
R/studio_builder.R contains three fully implemented internal
functions (sframe_builder_empty_state, sframe_builder_state_from_instrument,
sframe_builder_validate_draft) used by SurveyStudio startup and draft
validation.
SHA-256 hashing in the SurveyBuilder HTML includes a pure-JavaScript
fallback for environments where crypto.subtle is unavailable on
file:// origins, including common Firefox file:// configurations.
Saving a .sframe file from the builder now always succeeds.
The SurveyBuilder's rqSuggest box now appears with an icon and a
plain-language recommendation when two or more variables are selected in
the RQ modal.
The undo and redo buttons in the SurveyBuilder topbar are now correctly disabled when their respective history stacks are empty.
export_google_sheet() now writes Google Apps Script using JSON-encoded
JavaScript literals instead of interpolating instrument metadata directly
into executable code. The generated endpoint also rejects missing,
over-large, and non-object JSON POST bodies..sframe and .csv
files by extension, size, and text-content checks before passing them to
import functions.read_sframe() now validates the top-level .sframe payload structure
before hash verification and object reconstruction.on.exit() even when rendering fails.validate_sframe(), score_scales(), codebook_report(), cfa_syntax(),
and launch_builder(open = FALSE) have fully runnable examples.launch_builder_demo(), launch_studio_demo(), and
launch_dashboard_demo()) open in the browser with the demo instrument,
scales, and analysis plan preloaded, so no manual file loading is needed.demo("survey")) walks through the whole
workflow with step-by-step prompts.sf_instrument(), sf_item(), sf_choices(),
sf_scale().write_sframe(), read_sframe() with SHA-256 integrity
checking.render_survey().launch_builder().read_responses().