theoryforge (development version)
citation("theoryforge")and the About page name the package’s publisher, in full: “Comprehensive R Archive Network (CRAN)” stands before the DOI in the reference, and the BibTeX entry gives it aspublisher. The reference named no publisher before. Every reference across the package family and the author’s website now names CRAN in the same words.citation("theoryforge")and the website give the DOI that CRAN assigned to the package, https://doi.org/10.32614/CRAN.package.theoryforge, in place of the Zenodo concept DOI. It names the R package across all its versions, and it is the DOI that the CRAN page and the citation R generates for a CRAN package already gave, so the R package no longer circulates under two. Zenodo still archives every GitHub release of the repository, which carries the Python twin as well.CITATION.cffdescribes both packages, so it keeps the Zenodo DOI, which favours neither, and now lists each package’s identifiers, marked with the package they name. Its release date is now the day 0.6.0 first became public, when the Python package was published on 2026-08-27, where it gave a date on which no release of 0.6.0 appeared. The CRAN DOI and the Zenodo DOI are shown as full links on the website and in the README. The README gains a badge for the DOI CRAN assigned, for GitHub and CRAN, which the home page leaves out of its sidebar, where the DOI is already given under Citation.The literature article’s recipe for turning a scopusflow corpus into a theoryforge corpus keys cited works by Scopus identifier, folds DOI and keyword case and writes a missing year as null. The earlier recipe split one cited work into several nodes, so co-citation maps and keyword themes came out empty. The recipe is now a function,
scopus_corpus_to_tf(), shown in full and run by the test suite on a stand-in corpus.tf_read()andtf_read_corpus()now read a file exactly as the Python twin does. A YAML sequence is always returned as a list, so a file withmaturity: [draft]orrelation: [increases]is refused bytf_validate()as it is in Python. The one-element sequence used to collapse to a string, and the file passed. Unquotedy,Y,nandNused to become logicals and now stay strings, as do the R-specific missing-value forms.na,.na.real,.na.integerand.na.character. A number written with a comma, such as1,000, is now a string and an integer too large for an R integer a double, whereyamlread both asNAwith a warning. A merge key lets the mapping’s own key win, a byte-order mark is ignored and a missing final newline no longer warns. A repeated key in a JSON file is refused with the message YAML files already gave, and a file holding only an empty sequence ([]) is refused as not a mapping.tf_write()no longer loses information. Numbers keep 15 significant digits in both formats, where JSON rounded them to four decimal places and YAML to seven, so a severity of 0.49996 no longer comes back as 0.5. Logicals are written astrueandfalse, no longer asyesandno, and a missing value (NA) is written as null, so both twins read it back as missing.yamlwrote the R-specific.naforms, andtf_read()now reads those as text. Every field the schema types as an array of strings, such asderives_from: [p3], is written as an array even when it holds one string, so a theory written and read back validates against the package’s own schema.The
yamlpackage is now required at version 2.3.8 or later.tf_severity()is described for what it is, a pre-data ranking of the form of each prediction’s claim. Its documentation no longer cites Mayo (2018), whose severity is a property of a test and its data, and the 0.25 directional discount is documented as the package’s convention, not Meehl’s. The heading over its values in the preregistration and the dossier readsSeverity (pre-data rubric of claim form), and the severity chart is titledPre-data riskiness. The values are unchanged.The four prediction types are defined in the schema, in
?tf_add_predictionand in the methodology article, and the bundled panic predictions now state the value and tolerance that theirpointtype claims. The schema also marks a prediction’srisk_scoreand a test outcome’sseverity_at_testas informational fields that no function reads.Malformed input is now read as the Python twin reads it, by every function that takes a theory. A collection written as a mapping, including a named list built in memory, is read as empty, so
tf_check()on such a theory now reports the gate Python reports where R used to score the mapping’s values. A collection written as a single string is read as empty too. Numbers and logicals in text fields read as empty strings, soid: 2026andlabel: Yesno longer print as2026andTRUE. An entry of a string array that is not a nonempty string is ignored.derives_from: [~]therefore fails the derivation check, which it used to pass while the preregistration printed no derivation, and[[adults]]is no longer flattened toadults.relation,type,maturityandformal_model$typeoutside their allowed values, sequences such as[increases]included, are read as absent. They add no edge, claim form, advisory gate or formal model and print as empty, so a formal-model type such as"bayesian"no longer passes formalisation.tf_validate(full = TRUE)no longer stops with “subscript out of bounds” when a collection entry is a scalar, andtf_implications()andtf_simulate()no longer refuse two constructs without ids as duplicates. A theory that matches the schema gives the same results as before, except that an empty or blank entry of a string array, such asmeasurement: [""], no longer counts as an entry.tf_check()refuses a prediction severity that is infinite or lies outside [0, 1]. A severity of 7 gave an aggregate score above 100, and an infinite one an aggregate ofInf. It also decides each threshold item from its rounded score and sums severities by an explicit loop, so the verdict no longer depends on the platform’s floating-point accumulator: severities of 0.6, 0.7 and 0.2 pass the 0.5 threshold on every platform. Rounded values no longer print as-0.0.tf_simulate()validates its arguments with the Python twin’s messages and stops with a message naming the step and state when the trajectory diverges, where it returned NaN rows, rows of the wrong length or a step count it had truncated.steps = 2.5ran two steps,init = c(1, 2, 3)gave rows of nine values, a negativedtwas accepted and a NaN knob gave rows of NaN. Code that read the NaN rows of a diverging run now receives an error. The help page and the methodology article state the step condition. In a network without feedback loops, the explicit step explodes oncedt * dampingexceeds 2, so a largerdampingis no remedy.Text is normalised as in the Python twin. Both trim one set of Unicode whitespace characters, lowercase ASCII letters only and fold accented Latin letters through a fixed table, so
naïveno longer vanishes from a definition andÉmotionno longer matchesmotion. A lone no-break space no longer counts as a definition, a mechanism or a provenance detail, andtf_compile_sem()names the indicatorMüller scalemuller_scale, where it gavem_ller_scale.tf_new_evidence_dois()recognises DOIs written asdoi: 10...,DOI 10...,doi.org/10...,https://www.doi.org/10...or percent-encoded, and ignores a trailing full stop, comma or semicolon.Construct definitions and keywords in Greek, Cyrillic and other non-Latin scripts are now tokenised. They were deleted, so two identical Russian definitions scored no overlap and passed the redundancy screen. Greek and Cyrillic are compared without case or accents, other scripts as written.
tf_litmap()andtf_landscape()count pairs in linear time, so a fetched corpus with references maps in seconds instead of hours. A single record with 300 references took about a minute and now takes a fraction of a second. Results are unchanged.tf_landscape()no longer computes the co-citation map, since it never used it, andtf_lit_diagram()writes the DOT of a large map in one pass, where it copied every line written so far for each edge it added.tf_litmap(),tf_landscape()andtf_lit_diagram()check their arguments and the corpus with the Python twin’s messages.min_linkmust be a positive integer: 2.5 was truncated to 2,NAgave an empty map,c(2, 3)was recycled, and 0 or a negative value, which behaved as 1, now raises. A corpus without arecordslist, a misspeltrecrods:or records keyed by id included, is refused where it gave an empty map, and so is a record that is not a mapping. Integer keywords and references are kept as decimal strings, never in scientific notation, and one of 2^53 or more is refused, since R cannot hold it exactly and the twins would key it differently. A logical, a fraction or a nested value is refused with a hint: an unquotedNOoronin YAML is read as a logical, which R wrote as"FALSE"or"TRUE".tf_litmap()gainsmin_cocitation, a threshold for the co-citation map alone (by defaultmin_link), andtf_lit_diagram()gainsmax_edges, which draws only the strongest edges. Co-citation maps of real corpora are large: 200 OpenAlex records give over 11,000 edges at a threshold of two.tf_landscape()reports the words behind every match, infocal_termsandalternative_terms, and three kinds of word no longer decide a match. Words in the keywords of more thanmax_token_shareof the records, a new argument set to 0.5 by default, are shared by most of the corpus and are reported asfield_tokens. The words of the theory’s title name the phenomenon that every account explains and are reported asphenomenon_tokens. Words such as “theory” and “model” name a kind of account. “Panic” and “disorder” from the title used to make a theme on genetics crowded, and “theory” alone matched a theory of panic to a corpus on ego depletion. The title no longer supplies matches, so a construct word that also appears in it no longer matches either. The statuses are described as what they are, a count of the registered accounts that address a theme. A crowded theme calls for predictions that discriminate between its accounts, and is not a finding of redundancy. On the bundled corpus, the statuses are unchanged.tf_litmap()warns when one theme holds more than half the linked keywords, which happens on real corpora: seven OpenAlex corpora of 100 to 600 records each gave one theme holding at least 98.8 per cent of them. Such themes, and any landscape built on them, are not informative.tf_landscape()gives the same warning.tf_litmap()andtf_landscape()gainmethod = "simple_centres", the co-word clustering of Coulter et al. (1998) and Cobo et al. (2011), which gives bounded themes with centrality, density and a strategic-diagram quadrant on real corpora.tf_litmap()also gainsmin_theme_size,max_theme_sizeandmax_df, which excludes keywords shared by most records.methodis the third argument oftf_litmap(), so amin_cocitationgiven by position must now be named. Components remain the default for this release, and the giant-theme warning names the new method. The default is planned to become"simple_centres"in the next minor release.A frozen OpenAlex corpus of 150 works on panic disorder,
openalex-panic-2026.corpus.yaml, ships with the package beside the example theories (seetf_example_path()), so that the two methods can be compared on a real corpus. It is offered under CC0, as OpenAlex’s data are.tf_validate(full = TRUE)now checks the whole schema. It reports a missing required field of an assumption, an alternative, a piece of evidence or a test outcome, and apassedthat is notTRUEorFALSE. It also reports any field of the wrong type, an evidence direction or a formal-model type outside its enum, a malformedversionblock, aschema_versionnot of the form"1.0"and a number outside 0 to 1 where the schema asks for one. A file withpassed: "true", an evidence direction ofsupportsor a formal-model type ofbananaused to validate. Three conveniences are kept on purpose. ANULLoptional field is absent, a single string stands for a one-element array of strings andlist()stands for an empty mapping or sequence.tf_validate(full = TRUE)also reports an entry of a string array that is not a nonempty string, such as the null inderives_from: [p1, ~]or the empty string inmeasurement: [""]. Every other function ignores such an entry. The schema allows an empty string there, so a theory that matches the schema can now fail full validation for this reason alone.tf_validate()names a value of the wrong type. A required field holding a number, a logical or a list is reported as<field> must be a string, with a reminder to quote a number or a logical in YAML, where it used to be called missing.maturity: [draft]therefore givesmaturity must be a string. A scalar or a mapping where a collection belongs is reported as<key> must be a list.missing/emptyis kept for a field that is absent,NULLor blank, and a singleNAreads as absent, sincetf_write()writes it as null.tf_check()andtf_appraise_amendment()refuse a test outcome whosepassedis present and notTRUEorFALSE. A quotedpassed: "true"used to read as a failure, so an assumption added to protect the prediction counted as ad hoc and an amendment that should be progressive came out degenerating.tf_preregister(),tf_dossier()and the diagrams that score the checklist refuse it too, since they calltf_check(). A missing orNULLpassedstill reads as not passed.tf_simulate()gainsmethod = "exact", which propagates the linear system with its matrix exponential and so has no step-size limit. The default stays"euler"for this release and warns when its steps depart from the exact solution by more than 5 per cent. The default will change to"exact"in the next minor release. The Euler steps turned a decaying theory into an alternating explosion oncedt * dampingexceeded 2, and inflated a sustained oscillation into growth even atdt = 0.1. The record now names the method (method), the propositions that couple nothing (ignored: moderates, associates and propositions with an endpoint that is not a declared construct) and the pairs whose increases and decreases offset each other (opposed). The help page and the methodology article state what the model leaves out: one gain for every coupling,causesandmediatestaken as positive,functional_formnot read and a common initial value. Being linear, the model cannot show bistability. Matrix joins Suggests for a test that checks the propagator againstMatrix::expm().tf_fetch_corpus()stops with the HTTP status when OpenAlex refuses a request, where a rate-limit or permission error returned an empty corpus that read as a literature with no themes. OpenAlex’s own message follows the status when the response carries one. A response without a results list, which read as no results, is now refused. The function accepts an OpenAlex API key (api_key, by default theOPENALEX_API_KEYenvironment variable), sent as a header, records where and when the corpus was fetched insource, keeps each work’s DOI and can page throughmax_recordsresults.mailtois documented as ignored, since OpenAlex replaced its polite pool with API keys.The tests need
testthat3.1.7 or later.tf_appraise_amendment()now compares the content of two versions, not their prediction ids. A renamed prediction, with the same statement and type under a new id, is no longer new. A prediction derived only from propositions the prior already held, and protected by no new assumption, is reported as an articulation and does not make an amendment progressive. A prediction counts as corroborated only when no outcome refutes it, and a corroborated prediction that is dropped blocks a progressive verdict. An assumption added for an anomaly is ad hoc unless a prediction it protects that is new in this version, other than the anomaly, is corroborated. The result keeps its four fields first and adds the evidence behind the verdict:articulated,underived,corroborated_new_registered,renamed,dropped,dropped_corroborated,content_lost,new_anomaliesandassumptions. The amended panic example now adds a proposition, so its verdict remains progressive for a reason the appraisal can see, and its aggregate score moves from 87.1 to 87.7.?tf_add_assumptionand the schema defineadded_foras the id of the prediction whose anomaly an assumption answers, where the help page called it a reason. The methodology article no longer attributes theneutralverdict to Lakatos, whose scheme has only progressive and degenerating problemshifts. The bundled examples record the builder names in their provenance (tf_add_construct,tf_add_proposition,tf_add_prediction).tf_osf_push()stops when OSF refuses the upload, where it returned the failure as a completed upload. Any status outside 2xx, a 501 included, stops withOSF upload failed with HTTP <status>, and a 409 adds that a file of that name already exists. It gainsoverwrite, which adds a new version of an existing file of the same name: the folder is listed first and the dossier goes to that file’s upload link. Without it a second deposit under the same filename still fails, so pass a version-specificfilenameoroverwrite = TRUE. Dry runs match the Python twin for an empty node, an empty filename and filenames containing percent signs, whichURLencode()used to pass through without encoding anything, spaces and&included. A live deposit now sends its requests through curl, astf_fetch_corpus()does, and httr leaves Suggests (httr itself depends on curl).tf_render_report()escapes the title for YAML, so backslashes and quotes survive. A backslash in a title used to stop Quarto with a YAML error, and\emphturned into an escape character. Double quotes are now kept, where they became apostrophes, and an empty orNAtitlemeans the default, as an empty one does in Python. Quarto reads the title as Markdown, so raw TeX is dropped from HTML output and$\alpha$becomes mathematics.tf_report(format = "html")escapes every value it writes into the HTML, the theory id included.The rigour checklist moves to version 2.0, which every report records as
checklist_version. An item with nothing to assess is reported asn/a, with aNULLscore, and left out of the aggregate, which is now the weighted mean of the applicable items. The report gainscoverage, the share of the checklist’s weight that was applicable. An empty theory scored 26 and now scores 0, and adding a prediction to it no longer lowers its score. Parsimony no longer penalises declared auxiliary assumptions. It fails only when an assumption added in response to an anomaly has no independent corroboration, and it isn/awhen no assumption was added that way. Non-redundancy follows the redundancy screen’s flags, and the screen also flags a definition contained in another (tf_redundancy_check()gains anoverlapcolumn), so the deliberately redundant pair in the weak example is now caught. Mean severity counts every prediction, using the claim-form rubric where none is declared, andtf_add_prediction()gainsseverity. Causal testability counts every directed relation,mediatesandmoderatesincluded. Most well-specified theories score a few points higher (the bundled panic example moves from 84.8 to 87.3) and the weak example lower (from 12.0 to 2.2).tf_dossier()gives the checklist coverage, names the blockers that failed, printsn/afor an item with nothing to assess and sets a prediction’s declared severity beside the rubric’s value, with a note when the declared value exceeds the rubric’s by more than 0.2.tf_report(format = "html")printsn/afor such an item, the development roadmap leaves it out and the rigour grid shows it in grey. The preregistration of a theory without predictions no longer readsDerivation chain verified: yes.tf_embedding_redundancy()takes its default threshold from the checklist’s newembedding_similarity_max, 0.85 as before, since how high a cosine runs depends on the embedding model.The methodology article quotes the checklist’s criteria and corrects the causal testability row. It calls the five thresholds the package’s defaults where it called them calibrated, and no longer says that a theory passing every item scores 100. It explains that the aggregate is compensatory, so a blocked theory can outscore one whose gate passes, that the weights and thresholds are fixed and that the lexical screen cannot detect empirical redundancy (Rönkkö & Cho, 2022). Diagnosticity and the rubric’s bonus are described as resting on a declared rival, and the derivation chain as a check of references. The article also notes that a prediction without a declared severity is credited for its type and its named rival twice, in prediction severity as well as in precision and diagnosticity.
?tf_redundancy_checkgives its references’ full titles.inst/REFERENCES.bibdrops Forster and Sober (1994), which no item cites any more, and adds Rönkkö and Cho (2022).tf_implications()reads every relation the theory states.mediatesandmoderatesare directed edges andassociatesis a bidirected edge, covariance the theory leaves unexplained, and the statements are derived by m-separation, so the function no longer asserts independencies that the theory’s own propositions deny. Withxassociated withy, the chainz -> x -> yused to givez _||_ y | x. A pair that no set of constructs separates is listed in the newinseparablefield, and the record also gainscriterion,n_bidirectedandfeedback. For a theory with onlycauses,increasesanddecreases, the statements are unchanged, and so are those of the bundled theories. Amediatesormoderatesproposition naming an undeclared construct is now refused, as a causal one was, and so is a cycle that one of them closes. In the stereotype-threat app example, domain identification moderates test performance, so the function gives three more statements and widens the conditioning sets of two others. Thecausal_dagview oftf_diagram()exports the same graph, witha <-> bfor an association, and the nomological net draws an association without arrowheads.The documentation no longer says that a cyclic theory implies nothing testable or that dagitty rejects its
causal_dagexport. dagitty accepts the export but reads it by d-separation, which a model with feedback is guaranteed to satisfy only in special cases, a linear model among them (Bongers et al., 2021).?tf_implications, the Developing and testing article and the README warn that a conditional implication tested on fallible measures is rejected too often, and point to latent-variable tests.tf_implications()gainscycles = "sigma", which derives the independencies a cyclic theory implies by sigma-separation (Bongers et al., 2021), valid when each feedback loop has a unique equilibrium. The record then names the criterion"sigma", says whether the graph is acyclic and lists the feedback loops. The bundled panic network implies that arousal and avoidance are independent given perceived threat. The default still refuses a cyclic graph, and its message names the option. A golden file of the sigma record for each bundled theory joins the parity check.The
causal_dagview oftf_diagram()quotes a construct id that dagitty or Graphviz would otherwise split or misread, such asself-efficacy,1arousalora.b, and stops on the idsnodeandgraph, which dagitty reserves. dagitty readself-efficacy -> task-persistence -> outcomeas five nodes and implied eight independencies wheretf_implications()gives one.tf_compile_sem()renames construct ids and indicator names that lavaan would refuse or misread, such asc-arousal,1arousal,NA,efaor7_point_likert_rating, with a comment recording each renaming. lavaan readc-arousal =~ q1as a latent variable namedarousal. A comment writes a control character or a character outside the Basic Multilingual Plane as<U+XXXX>, because lavaan could otherwise read part of a comment as a model line.tf_compile_sem()also refuses a name collision it used to write without a word, between two constructs, two indicators of one construct or a construct and an indicator. The bundled theories are unaffected. lavaan joins Suggests for a test that parses the renamed syntax withlavaan::lavaanify().tf_compile_sem()writes the covariances the theory fixes at zero, whichlavaan::sem()would otherwise free, so a fitted model can refute the implications the theory makes. Two constructs that are both exogenous or both terminal, with no association between them, are unrelated by the theory’s account, andlavaan::sem()frees their covariance by default. In the bundled modality-switching theory, those are two of the six independenciestf_implications()derives. The syntax now ends withc_sensorimotor_experience ~~ 0*c_lexical_familiarityandc_switch_cost ~~ 0*c_conceptual_access, and the model’s degrees of freedom rise from 59 to 61. A pair with a moderator keeps lavaan’s defaults, and deleting the block restores them all. Once a covariance line names an observed exogenous variable, lavaan treats it as random and fixes at zero each of its covariances left unwritten. The block therefore writes those of an observed moderator as free. A moderation used to lose the moderator’s main effect, andtf_compile_sem()now writes it, with a comment on the product term to add by hand. It also cautions that each feedback loop may not be identified, lists the constructs with a single indicator and nameslavaan::sem()as the target. The four bundledsem.lavaanfiles change.The Developing and testing article demonstrates
tf_compile_sem()on the modality-switching theory and no longer says that the panic network’s syntax can be passed directly tolavaan::sem(): its feedback loop leaves the model unidentified.?tf_compile_semsays that the syntax is a starting point for a fit. Indicator names must match columns of the data, a single-indicator construct is treated as measured without error, and manipulations and categorical predictors with more than two levels have to be re-specified by hand.tf_preregister()no longer says that a derivation chain was verified. The check behind the line only finds that each prediction cites declared propositions, so the line now readsDerivation references resolve, withyes,noornot applicable (no predictions). Each hypothesis lists the auxiliary assumptions that protect it, the registered rivals it discriminates and its test record, so a hypothesis that a recorded outcome has already tested is marked as such, with each outcome’s result, date and registration. Both corroborated hypotheses of the amended panic example used to read as hypotheses still to be tested. The header gives the version and its parent, and a new section lists the conditional independencies thattf_implications()derives with its default settings, or says why there are none. The Developing and testing article calls the output the hypotheses section of a preregistration, to be completed with the design, sampling plan, analysis plan and inference criteria (Bosnjak et al., 2022), where it called it suitable for deposit. The fourprereg.mdand fourdossier.mdgolden files change.tf_dossier()adds a test record, which lists every test outcome with what it found and the observation, date and registration it records. It also adds the evidence, with the prediction each entry supports, its direction and its DOI. The implied independencies appear once, in the appended preregistration.The
pipelineandworkflowviews oftf_diagram()show every outcome of a replicated prediction. The outcomes of one prediction were declared as one node, which Graphviz merged, so the figure showed only the last result and a passed replication hid an earlier failure. The second and later outcomes are nowresult_<id>#<k>(outcome_<id>#<k>in the workflow), each with its own edge, and when a prediction has several outcomes, each label carries the outcome’s date. A prediction tested once is drawn as before.When the site is built, the methodology article reads the criterion column of its table of rigour checks from the checklist the package ships, so the table follows the checklist. A check that the checklist gains without a name in the article stops the build, and a test evaluates the chunk.
New
tf_post_data_severity()computes how severely a bounded prediction passed its test, from the outcome’s estimate and standard error (Mayo & Spanos, 2006), using new optionalboundson predictions andestimateandseon test outcomes. It complements the pre-data rubric and does not enter the checklist. The value is the smaller ofpnorm((estimate - lower) / se)andpnorm((upper - estimate) / se)over the finite bounds, so a one-sided claim leaves one bound open, and with two finite bounds it equals 1 minus the p value of the two one-sided tests of equivalence.tf_validate(full = TRUE)checks the new fields, andtf_dossier()gives each value it can compute in its test record. The modality-switching example gives its interval predictionpred2bounds of 20 and 60 ms and records one outcome whose values are labelled as illustrative, so its preregistration, dossier and pipeline and workflow diagrams now show a passed test.tf_litmap()keys a reference that holds a DOI by the DOI, lower-cased and without its resolver prefix, astf_new_evidence_dois()compares DOIs. One cited work written as10.1016/J.BRAT.2015.10.002,doi:10.1016/j.brat.2015.10.002andhttps://doi.org/10.1016/j.brat.2015.10.002is therefore one node of the co-citation map, where it made three, and co-citation labels of DOI-keyed corpora are now lower-case DOIs. Other references, such as Scopus and OpenAlex identifiers, and all keywords are compared as written, as before.tf_landscape()reads no references, so its results are unchanged. The corpus schema now ships with the package, atsystem.file("schema", "corpus.schema.json", package = "theoryforge").Predictions accept an optional
testblock recording the planned sample size, alpha, smallest effect of interest and power, andtf_preregister()lists it in a Sampling plan section. The block is recorded, not scored: power does not change a prediction’s severity.tf_add_prediction()gainstest, andtf_validate(full = TRUE)checks the block. The modality-switching example records a plan forpred2whose values are labelled as illustrative, so its preregistration and dossier gain the section. A theory without a plan gives the same document as before.The theory schema the package ships, at
system.file("schema", "theory.schema.json", package = "theoryforge"), compiles under strict JSON Schema validators such as Ajv. Its note on the closed top level was acommentkeyword, which JSON Schema does not define, and is now a$comment. Three descriptions promised behaviour the package lacks and now describe what it does.tf_validate()requiresschema_versionandtf_validate(full = TRUE)checks its pattern, but no function gates loading on the version. Atdraftmaturity the gate is advisory, while failed blocker items are still reported asfailand still count in the aggregate score. The checklist applies the same twelve items to everytheory_form.The spelling test now fails a check run with
NOT_CRANset, as indevtools::check()and on GitHub Actions, when the documentation uses a word that neither the British English dictionary norinst/WORDLISTknows. It used to list such words and carry on, so every check on GitHub Actions ended with a NOTE that left the run green and would have hidden a new one. CRAN and a plainR CMD checkstill skip the test. The word list gains the 47 words that had slipped past it, none of them a misspelling. The help fortf_implications()now sets a view name as code, and a sentence of the development article no longer uses a possessive the checker cannot read.
theoryforge 0.6.0
CRAN release: 2026-09-01
New
tf_implications()derives the testable implications of a theory’s causal subgraph. It reads the causal propositions as a directed graph, checks that the graph is acyclic, and returns the basis set of implied conditional independencies: one claim per pair of constructs with no causal relation between them, conditioned on the parents of both, in the notation dagitty prints. That set is the shortest complete statement of what a causal theory forbids in data, so it is what a study can be designed to refute. The package cited the derivability of those implications in its own checklist and derived none of them. A cyclic graph has no basis set and is refused with the cycle named, which is what happens to the bundled panic-network example and its amended version. A theory with no causal relations comes back with an empty set and no error. The Python twin gainstheory.implications(), returning the same records in the same order. The derived sets were checked againstdagittyandggm, which sit in Suggests for that purpose and whose tests skip when they are absent.A fourth example theory ships with the package,
modality-switching.theory.yaml, and it is the worked example fortf_implications(). Both panic-network fixtures are cyclic, so until now every bundled theory showed only what the function refuses. This one states the modality-switching effect in grounded conceptual processing: sensorimotor experience with a concept drives activation of the modality-specific perceptual system, which raises the cost of switching modality between consecutive trials and eases conceptual access, as lexical familiarity with the word form does too. Five constructs and four causal propositions give an acyclic graph with a fork and a collider in it, and a basis set of six conditional independencies, confirmed againstdagittyandggm. The panic fixtures stay as they are: a feedback loop is legitimate theory, and the refusal is worth seeing as well, so the Developing and testing article now shows both outcomes.New
tf_example_names()andtf_example_path()reach the theories and the literature corpus bundled with the package, mirroringexample_names()andexample_path()in the Python twin, so the README quick start runs straight afterremotes::install_github()with no clone.tf_validate()refuses an unrecognised top-level field. A misspelt collection key such aspredicitions:was dropped without a word, taking its whole collection with it and moving the aggregate score and the gate. The schema’sadditionalPropertieswas set to match, so a third-party validator agrees.Four further refusals replace a silently wrong answer.
tf_read()andtf_read_corpus()no longer accept a top-level YAML sequence of mappings, a shape that used to read as a document with every collection empty.tf_simulate()refuses duplicate construct ids, which produced two different but equally plausible trajectories from one file.tf_check()refuses a non-numeric prediction severity, where it used to coerce one, andtf_validate(full = TRUE)reports the same file as invalid, so the scorer and the validator agree about it.tf_embedding_redundancy()refuses a pair of unequal-length vectors, naming the constructs and the lengths, where it used to recycle the shorter one.An enum field written as a YAML sequence, such as
theory_form: [network], is now refused.%in%unboxed the one-element list, so the file validated in R and was refused in Python.tf_check()andtf_dossier()recordchecklist_version, the version of the checklist whose weights and thresholds produced every number in the report, so two reports written against different checklist revisions are no longer silently comparable.tf_simulate()echoes backk,dampingandinitalongsidedtandsteps, so a recorded trajectory can be reproduced from what the record itself reports.The causal-testability criterion now describes what it computes. It asserted acyclicity and never checked it. The criterion and the methodology article now state that the export is emitted as written, that it is not verified acyclic, and that the shipped panic-network example is in fact cyclic. No score, gate or status changed. The check the criterion once implied now lives in
tf_implications(), and it can refuse a graph outright instead of quietly rescoring it.Every file the package writes goes through one LF-only, UTF-8 writer, so the R half no longer emits CRLF where the Python half emits LF.
tf_write()forces UTF-8 as its sibling writers already did, and a failedquarto renderno longer returns its output path as though it had succeeded.The network adapters carry the same 30-second timeout as their Python counterparts, and both languages reject a
per_pageoutside OpenAlex’s documented 1-200 range before making a request.Every vignette now turns console colour off and fixes the console width while it renders. pkgdown passes the calling terminal’s colour support into its build subprocess, and the Get started vignette’s failure path therefore published the
tf_validate()error with its bold and yellow escape sequences showing as literal text around the words Error and the exclamation mark.inst/WORDLISTis read at last:spellingjoins Suggests and atests/spelling.Rruns the check underR CMD check.
theoryforge 0.5.0
- The
development_roadmapview is rebuilt around a theory hub carrying the title, the aggregate score and the gate. Items are ordered blockers first and then by weight, each labelled with its ordinal, the checklist criterion and whether it blocks the gate, with visible edges down the blockers and the advisory items set three abreast. - The three SVG chart views (
venn,rigour,severity) now declare awidthand aheightalongside theirviewBox, so each renders at its natural size wherever it is embedded. Without an intrinsic size a chart was stretched to the width of its container, and since the three views have different natural widths the same declared 13px label came out at a different size in each one. - The
venndiscs take the construct-border teal for their outline in place of the former navy, which fell below the 3:1 contrast floor for graphical objects on a dark page and left the figure close to invisible under the dark theme. - The bundled
panic-networkfixtures give the three constructs distinct boundary conditions, so thevennview drawn from them shows where construct scopes diverge, where it used to put a zero in six of its seven regions. - All of the above are mirrored byte for byte in the Python twin.
- Documentation: the Get started vignette shows what
tf_validate()returns and demonstrates the failure path, and the development article runstf_osf_push()in its default dry-run mode, where it was previously withheld.
theoryforge 0.4.0
- The DOT diagram views are redesigned for content and legibility. Every view opens with a shared Meridian style prelude (Helvetica type, role-coloured rounded nodes); labels wrap so nodes stay narrow; workflow and pipeline nodes carry the id together with the relation or type, where a bare word stood; the development roadmap stacks its items in a single column; and the theme landscape colours themes by status. Every view fits a documentation column. The intermediate representation stays byte-identical to the Python twin’s.
theoryforge 0.3.0
- New
tf_render_diagram()renders the digraph views without leaving R: a DiagrammeR widget for the viewer and R Markdown, or a standalone SVG string withas = "svg". It accepts a theory or a raw DOT string, sotf_lit_diagram()output renders the same way; the three SVG chart views pass through unchanged, andcausal_dagis refused with a pointer to dagitty. The rendering packages (DiagrammeR,DiagrammeRsvg,htmltools) are in Suggests, so the deterministic core stays dependency-free, and rendering sits outside the cross-language parity contract. The articles now show each digraph rendered beneath its intermediate representation.
theoryforge 0.2.0
- The severity chart is re-laid out: bars start just past the longest row label and each value trails its own bar. The diagram intermediate representation for
tf_diagram(type = "severity")changes accordingly, and it stays byte-identical to the Python twin’s. - Documentation: the articles now show the
provenance,development_roadmap,pipelineandco_citationviews, the embedding-redundancy screen,tf_validate(full = TRUE)and the remaining build verbs, and a new section covers rendering and depositing.
theoryforge 0.1.0
First public release. The package provides a reproducible workflow for building, developing and testing scientific theories, with behaviour pinned by a shared specification (API_SPEC.md) so the R and Python twins return identical verdicts and byte-identical diagram intermediate representations.
- Core: theory-object input, output and structural validation; a 12-item rigour checklist with a weighted aggregate score and a blocker gate; diagram intermediate representations (nomological net, provenance, causal DAG); and a deterministic lexical construct-redundancy screen. Where the schema expects an array of strings, a nonempty scalar string is read as a singleton list (API_SPEC.md section 4), so natural YAML such as
derives_from: p1yields the same rigour verdict and gate as the Python twin; an empty or whitespace-only scalar counts as absent. - Workflow modes: a builder API with auto-logged provenance (BUILDING); an operationalised severity rubric and preregistration export (TESTING); and a Lakatosian progressive-versus-degenerating amendment appraisal (DEVELOPMENT).
- Literature layer: a deterministic bibliometric mapping (
tf_litmap,tf_landscape,tf_lit_diagram), a parity-exempt OpenAlex corpus adapter, and a deterministic, dependency-free check for DOIs not yet cited by a theory (tf_new_evidence_dois), for use with a search from any source, including the companionscopusflowpackage.tf_lit_diagram()lists the valid types in its unknown-type error, matchingtf_diagram(). - Testing and review: lavaan model-syntax compilation (
tf_compile_sem) and a reviewer-facing audit dossier (tf_dossier). - Simulation, reporting and deposit: a deterministic dynamical-system runner (
tf_simulate), a Quarto report wrapper (tf_render_report), an opt-in embedding redundancy screen (tf_embedding_redundancy), and an OSF deposit adapter (tf_osf_push, dry-run by default).tf_osf_push()percent-encodes the filename component of the upload URL, keeping the dry-run request identical to the Python twin’s. - Cross-language determinism: the literature layer and the amendment appraisal sort with radix (codepoint) ordering regardless of locale, matching the Python twin for mixed-case keywords and ids.
- Metadata:
citation("theoryforge")and the About article read the package version from the package metadata.