Changelog¶
All notable changes to theoryforge (the R and Python twin packages) are documented here.
The format follows Keep a Changelog. The two packages share a
version and a single behavioural contract
(API_SPEC.md).
Unreleased¶
Fixed¶
- The Python builders split a single string into characters.
derives_from="p1"was stored as["p", "1"], which blocked the gate, andmeasurement="heart rate"compiled to one indicator per character. A single string is now a one-element list, as in R. A template whose collection keys are present with no value (constructs:) no longer raises AttributeError. A collection that is not a list is refused with a TypeError that names the key. - The same file no longer gives different theory objects in the two languages. R read
unquoted
yandnas booleans and collapsed a one-element YAML sequence to a string. Python read an unquoted date as a date object, which then could not be written to JSON, kept the second of two repeated keys without a word and failed on a byte-order mark in JSON. Both readers now follow one rule set, pinned in API_SPEC section 3 with a table of the scalars that once diverged. Sequences are lists, andy,Y,n,Nand dates stay strings. Integers are decimal, octal or hexadecimal only, so1:30and1_000are strings where Python read 90 and 1000, and-.5is a number in both. R read1,000and an integer beyond its integer range as NA, with a warning. It now reads the first as the string Python reads and the second as a number. A merge key lets the mapping's own keys win and lists them first, as R does. A byte-order mark is ignored. A repeated key in YAML or JSON is refused withDuplicate map key: '<key>', and when several mappings repeat a key both languages name the one in the mapping that closes first. The 0.6.0 fix for one-element enum sequences held for objects built in memory and for JSON, but not for YAML read by R. It now holds for files too. - Python's
write()quotesy,Y,nandN, so its files stay readable by theoryforge 0.6.0 in R. It also quotes the other strings a reader would take for something else, such as-.5and1,000, and ends JSON output with a newline. - R's
tf_write()rounded JSON numbers to four decimal places and YAML numbers to seven. It wrote logicals asyesandno, a missing value as R's own.naforms and one-element arrays as scalars that failed the package's own schema. It now keeps 15 significant digits, writestrue,falseand null, and writes every field the schema types as an array of strings as an array. - Malformed input made the two engines diverge or crash. Python raised AttributeError or
TypeError in nearly every function when a collection entry was not a mapping, printed
Nonefor null fields in the preregistration, the dossier and the diagram IRs, and crashed on id-less entries inappraise_amendment(). R scored the same files, stopped in full validation or read them differently, so the gates could disagree (55.4 against 84.8 on one file). Both twins now read every value through one set of accessors, pinned in API_SPEC section 3 ("Reading a theory"), so they read a malformed value the same way. A collection given as a mapping or a single string reads as empty. A text field that is not a string reads as"", so a number or a boolean no longer prints as1.0in one engine and1in the other.type,relation,maturityandformal_model.typeoutside their enum are absent, so a formal-model type outside the schema's enum no longer passes formalisation. A null entry inderives_frompassed full validation and then blocked the gate in Python while R passed it. Both now ignore the entry, so[~]fails the derivation check and[p1, ~]passes in both.implications()andsimulate()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. Ten malformed theories infixtures/edge/pin the agreement. check()accepted severities outside [0, 1], so a severity of 7 gave an aggregate score above 100, and Python raised OverflowError on an infinite one where R reported an aggregate ofInf. Both now refuse them with named errors, an infinity with the message already given for NaN. The pass/warn decision for mean severity compared an unrounded mean whose last bit depended on the Python version and the R platform, so severities of 0.6, 0.7 and 0.2 passed or warned by platform. Statuses now come from the rounded score and the mean is an explicit left-to-right sum. R no longer emits a negative zero.simulate()validates its arguments with one set of messages in both languages and stops with a message naming the step and state when the trajectory diverges. R returned NaN rows, rows of the wrong length or a step count it had truncated, and Python raised OverflowError or TypeError.steps=2.5ran two steps in R and raised TypeError in Python,steps=-1returned one row in Python,initgiven three values made nine-value rows in R, and both accepted a negativedt. On the panic fixture withdt=2,k=10,damping=0andinit=10, within the apps' ranges, Python raised OverflowError at step 228 where R returned rows of Inf and NaN. Both now stop there withsimulate diverged at step 228: state 'c_arousal' is not finite; reduce dt or k. Python accepts numpy integers and integral floats forstepsand refuses bools, as R refuses logicals. Python's products are summed left to right as in R: the builtinsum()compensates rounding error from Python 3.12, which moved fast-growing trajectories apart beyond the parity tolerance. The apps show the message where they showed a raw error or a "diverged" note, and passstepsas entered instead of truncating it. API_SPEC section 22, the docstring, the help page and both methodology pages state the step condition: in a network without feedback loops, the explicit step explodes oncedt*dampingexceeds 2, so raising the damping is no remedy.simulate()checks the initial state for divergence too. Aninitof 1e303 is finite, but a million times it is not, so the rounding of row 0 raised OverflowError, while R wrote a row of Inf or stopped at step 1 and blameddtork. Both twins now stop withsimulate diverged at step 0: state '<id>' is not finite; reduce init. The internalrnd(), used for every number theoryforge emits, is defined for every double as in R: a finite value too large to scale gives an infinity of its sign, where it raised OverflowError, and NaN gives NaN, where it raised ValueError. No value in range changes. API_SPEC section 3 now requires every floating-point sum to be a left fold in file order and corrects its account of the rounding bias, which stops absorbing last-bit differences once a value reaches about 1.7e4.- Text is normalised identically in both twins. Both trim one set of Unicode whitespace
characters, lowercase ASCII letters only and fold accented Latin letters through a
fixed table (
schema/fold.json), sonaïveno longer vanishes from a definition andÉmotionno longer matchesmotion. R accepted a lone no-break space as a mechanism or definition where Python refused it, and the two lowercased the Turkish dotted capital I differently, splitting scores, redundancy flags and the lavaan indicator names.compile_sem()now names the indicatorMüller scalemuller_scale, where it gavem_ller_scale, and a whitespace provenance detail is left out of the provenance diagram and the dossier in both.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. API_SPEC sections 3, 6, 18 and 19 pin the rules. - 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.
litmap()andlandscape()read the same corpus differently in the two twins. Amin_linkof 2.5 kept pairs counted at least 2.5 times in Python and at least twice in R, None raised TypeError where R'sNAgave an empty map, and"2"worked only in R. An unquoted integer id came out as an int in Python and a string in R, and a record mixing one with a DOI raised TypeError in Python. An unquotedNOkeyword (nitric oxide), which YAML reads as false, was dropped by Python and written asFALSEby R, andONraised TypeError. Records written as a mapping were counted by R and ignored by Python, and records that were not mappings raised AttributeError in Python and were dropped by R. A misspeltrecordskey gave an empty analysis in both. Both twins now check the arguments and the corpus with the same messages (API_SPEC section 14):min_link must be a positive integer(an int or an integral float, never a bool),invalid corpus: missing records list,invalid corpus: record[<i>] is not a mappingand, for a boolean, a fraction or a nested value,invalid corpus: record[<i>] keywords must be stringswith a hint about YAML booleans. An integer-valued entry becomes its decimal string in both. Amin_linkof 0 or below, which behaved as 1, now raises.- A corpus entry that is an integer of 2^53 or more is refused with
invalid corpus: record[<i>] <field> entry <k> is a number too large to be an exact identifier; quote it, since R cannot hold it exactly and the two twins would otherwise key it differently. Ten corpora infixtures/edge/pin the corpus rules, including Scopus-style ids beyond the 32-bit range written unquoted in YAML and JSON. - Full validation checked only part of the schema. A quoted
passed: "true", an evidence direction ofsupportsand a formal-model type ofbananaall validated, and the quotedpassedthen read as a failure in both languages. An assumption added to protect the prediction therefore counted as ad hoc, which turned the panic v2 amendment, given such an assumption, from progressive to degenerating.validate(full=True)now checks every required field, type, enum and pattern of the schema, the version block included, with the same messages in both languages and three conveniences that API_SPEC section 2 documents.check()andappraise_amendment()refuse apassedthat is present and not a boolean, and so do the preregistration, the dossier and the diagrams that score the checklist, since they callcheck(). A test run in CI holdsvalidate(full=True)to a JSON Schema 2020-12 validator. Python's full validation also raised OverflowError on aseveritywritten as an integer too large for a float, where R reported the value, and now reports it as R does. validate()called a present value of the wrong type missing. A number, a boolean or a list in a required text field now gives<field> must be a string, with a reminder to quote a number or a boolean in YAML, somaturity: [draft]givesmaturity must be a stringwhere it gavemissing/empty required field: maturity. A collection written as a single value or as a mapping gives<key> must be a list, where it used to validate and read as empty.- R's
tf_fetch_corpus()returned an empty corpus without an error when OpenAlex refused a request with HTTP 429 (rate limit) or 403, and an HTML error page stopped it with an opaque JSON parse error. Python raised urllib's own HTTPError, with different text. Both twins now raiseOpenAlex request failed with HTTP <status>, followed by OpenAlex's message when the body carries one. Python raisesOpenAlexHTTPError, a subclass ofurllib.error.HTTPError, so existing handlers still catch it. A response without aresultslist, which both twins read as zero results, raisesOpenAlex response has no results list. API_SPEC section 17 pins the messages. - Apps: after an upload that could not be read, the apps went on computing with the
previous theory while the reproducible code named the failed file, and a failed restore
left the same mismatch. The code now names a file only once it has loaded. A failed
upload keeps the active theory's summary under the message
Could not load <file>; the active theory is still <title>. A failed restore falls back to the first example, says so and leaves the saved operation unrun, since its result belonged to another theory. Uploads were never checked, so an empty mapping or a misspeltpredicitions:ran every operation without a word. Each load now runs full validation and the theory card showsvalidor the number of problems. An operation that fails on an invalid theory says so and offers Validate. Engine errors show their last line, with the full traceback in a collapsed section. A change of theory clears the result and the code computed on the previous one. - Apps: a browser that blocks site storage left the apps on a blank page, because the
theme toggle read
localStoragebefore start-up was guarded. Storage is now optional: the apps start, and the theme toggle works for the session without being remembered. A start-up failure shows its message even when it happens before the loading screen exists. The documentation site's landing and 404 pages guard their toggles the same way. - Site: under a dark operating-system theme, the landing page drew its links at 3.44:1
contrast against the background, below the WCAG AA minimum of 4.5:1. They now use the
dark theme's link colour (10.37:1). The landing and 404 pages read the
systemvalue the apps store as a theme of its own, which showed the wrong toggle icon and made the first click do nothing. They now apply a stored theme only when it islightordark. - Site: the deployed Python app carried the bytecode caches (
__pycache__, 21.pycfiles) that building the documentation leaves in the package source.apps/build.mjsnow copies only the file types the app's manifest lists. The 404 page links both apps. render_report()escapes the title as a YAML double-quoted scalar, by one rule in both twins (API_SPEC section 23), so backslashes, quotes and control characters survive. A backslash in a title used to stop Quarto with a YAML error, and\emphor$\alpha$turned into an escape character. Double quotes are now kept, where they became apostrophes. Quarto reads the title as Markdown, so raw TeX is dropped from HTML output and$\alpha$becomes mathematics.report(format="html")escapes every value it writes into the HTML, so a theory id such asa<b&cno longer reaches the markup raw.osf_push()raisesOSFUploadErrorwhen OSF refuses the upload, with the message the R twin now stops with,OSF upload failed with HTTP <status>, and for a 409 a hint that a file of that name already exists. It subclassesurllib.error.HTTPError, so existing handlers still catch it. R returned any refused upload, a 409 or a 501 among them, as a completed one. The dry runs of the two twins now agree for an empty node, an empty filename and a filename that holds a%XXsequence, which R did not encode. The token is sent as an unredirected header, asfetch_corpus()sends its key, so a redirect of the folder listing cannot carry it to another server.implications()ignoredmediates,moderatesandassociates, whichcompile_sem()andsimulate()read, so it asserted independencies that the theory's own propositions deny. Withxassociated withy, the chainz -> x -> ygavez _||_ y | x. Withaassociated withb, the collidera -> c <- bgavea _||_ b, the negation of the association. A construct that moderated two outcomes left them independent given their other causes. Every relation is now read through the relation table (API_SPEC section 28):mediatesandmoderatesare directed edges, andassociatesis a bidirected edge, covariance the theory leaves unexplained. The statements are derived by m-separation (Richardson, 2003), given the parents of the pair or, when those do not separate it, given its other ancestors. A pair that no set separates is listed in the newinseparablefield. The record also gainscriterion,n_bidirectedandfeedback. A theory with onlycauses,increasesanddecreasesgets the same statements as before, and so do the four bundled theories. Amediatesormoderatesproposition naming an undeclared construct is refused, as a causal one was, and so is a cycle that one of them closes. In the app examples, domain identification moderates test performance in the stereotype-threat theory, so that theory gains three statements and two of its conditioning sets widen. The planned-behaviour theory now states the associations among its three antecedents of intention that Ajzen (1991, Figure 1) draws, so the three statements that made them independent are gone. Thecausal_dagview exports the same graph, witha <-> bfor an association between two of its constructs, thenomological_netview draws an association without arrowheads, and the apps draw a bidirected edge as a dashed line with an arrowhead at each end. API_SPEC sections 5, 27 and 28 state the rules. The weak example'snomological_netgolden changes, and the edge-case records gain the new keys.- The
causal_dagview andcompile_sem()wrote construct ids verbatim. dagitty read the export ofself-efficacy -> task-persistence -> outcomeas five nodes and implied eight independencies whereimplications()gives one, and the apps' Graphviz rendering stopped at a hyphen or a dot and drew1arousalas two nodes. The view now writes an id bare only when DOT reads it as one identifier and quotes any other, and it refuses the idsnodeandgraph, which dagitty reserves even in quotes. In the syntaxcompile_sem()wrote, lavaan stopped atthreat ~ c-arousal, atNA =~ q1and at the indicators7_point_likert_ratingandefa, and it readc-arousal =~ q1as a latent variable namedarousal. It also merged two indicators of one construct that sanitise alike without a word, and read a construct named like another construct's indicator as a second-order factor.compile_sem()now renames such ids and indicators, with a comment recording each renaming, and refuses a name collision between two constructs, two indicators of one construct or a construct and an indicator, naming both. A comment writes a control character or a character above U+FFFF as<U+XXXX>. A line feed in an id would otherwise end its comment early and turn the rest into a model line, and on Windows two characters above U+FFFF in a comment made lavaan readoutcome ~ moodasutcome ~ mood. API_SPEC sections 5 and 19 state the rules. The bundled theories and the goldens are unchanged. - Apps: the amendment appraisal gave every theory the first example, version 1 of the
panic network, as its prior by default, and 0.6.0 reported three unrelated app examples
as progressive amendments of it. Predictions are matched by id, so a version of another
theory gives a meaningless verdict. The prior now defaults to the version the loaded
theory declares as its parent, when exactly one version listed is that parent. A
declared parent has the theory's
parent_idas its version id, and the theory's id begins with its id. Otherwise, no prior is chosen and Run waits for one, and the note under the selector says whether the theory names a parent at all. A prior that is not the declared parent carries a caution in the selector and in the result, and a prior version can be uploaded. Of the app examples, only the amended panic network has a default prior, its first version. - Apps: the apps did not offer the conditional independencies a theory implies, the
claims data can refute, and left out the modality-switching example, the worked example
of
implications(). Both apps now offer Implied independencies with thecycleschoice ofimplications(). A refusal, such as the default's for a feedback loop, is shown as a result with the package's message, and the help states what the statements assume. Modality switching is the tenth example. The apps remembered the chosen example by its place in the list and now remember it by its file. A session saved the old way is mapped once, and its prior is dropped, as it may be the default that app set. - Apps: the reproducible code read every file by its bare name, so pasted code stopped
with file-not-found, even for the examples the package ships. It now reads those, the
corpus and a shipped prior through
example_path(), and asks for any other file to be saved beside the script first. The Python code wrote its SVG files in the locale's encoding, which on Windows turns the ellipsis of a shortened prediction id into a byte no XML reader accepts. That broke the severity chart of four app examples, and the code now writes UTF-8. The R simulate comment lists all ten elements of the record, and the code panel says the code reproduces the result with the installed package, where it promised exactly what the app computed. - Apps: the checklist's guide said an advisory gate meant a theory usable with the noted gaps, and a draft theory failing both blocking items was told it cleared the gate with advisories. The gate is advisory at draft maturity whatever fails. The guide now says that pass means neither blocking item failed and blocked that at least one did. The reading puts an advisory gate down to draft maturity and names the blocking items that fail. The effort-recovery example promised oscillating trajectories that its default run does not show. It now says what the run shows, fatigue overshooting its resting level of zero while recovery peaks. It also names the settings that make the loop cycle.
- Apps: the number fields passed a fractional
stepsto the package, which refuses it. The glue truncated a fractionalmin_link, while the code showed the fraction. Whole-number parameters are now rounded, and each field shows the value a run uses. compile_sem()wrote a model thatlavaan::sem()does not read as the theory's own, so a fit could not refute some of 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 modality-switching theory, those are two of the six independenciesimplications()derives. The syntax now ends with a block that fixes each such covariance at zero,c_sensorimotor_experience ~~ 0*c_lexical_familiarityandc_switch_cost ~~ 0*c_conceptual_accessthere, which raises the model's degrees of freedom 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 compiled to a comment alone, so the moderator had no path to the outcome. It now gives the moderator's main effect, unless a path relation already writes that line, and a comment on the product term to add by hand. A comment cautions that each feedback loop may not be identified, without claiming that an instrument is needed: the effort-recovery app example is identified without one, and both panic-network models are not. A comment lists the constructs with a single indicator, whose residual variance lavaan fixes at zero, and the header nameslavaan::sem()as the target. API_SPEC section 19 states the rules. The foursem.lavaangoldens change, and so does thecompile_sementry of every edge-case record that holds one.preregister()no longer says that a derivation chain was verified. The checklist item behind the line only checks that each prediction cites declared propositions, so "Shoe size predicts vocabulary." citing a proposition about arousal read as verified. The line now readsDerivation references resolve: yes,noornot applicable (no predictions). A hypothesis that a recorded outcome had already tested was listed as a fresh hypothesis, as both corroborated predictions of the amended panic example were. It is now markedalready tested, with each outcome's result, date and registration.- The
pipelineandworkflowdiagrams declared every outcome of a prediction as one node, which Graphviz merged, so the figure showed only the last result and a passed replication hid an earlier failure. Each outcome now has a node and an edge of its own. The k-th outcome of a prediction, from the second on, isresult_<id>#<k>(outcome_<id>#<k>in the workflow), a name that no sibling id such ash1_2can produce, as a_2suffix could. When a prediction has several outcomes, each label carries the outcome's date. A prediction tested once is drawn as before, so no diagram golden changes. API_SPEC section 12 states the rule. - Apps: the preregistration's guide called the output ready to timestamp before data collection, and its reading said that the document preregistered every hypothesis, those already tested included. The guide now calls it the hypotheses section of a preregistration, to be completed with the design, sampling plan, analysis plan and inference criteria (Bosnjak et al., 2022). The reading counts the hypotheses already tested, and the dossier's help and guide name the test record and the evidence. The pipeline diagram's reading says that each recorded outcome has a node of its own.
- Python: two guides, the docstrings of
theoryforge.renderand the error thatrender_diagram()raises without graphviz gave the render extra's install command without quotes. In zsh, the default shell on macOS, the brackets are a filename pattern, so the pasted command stopped with "no matches found" before pip ran. The extra is now double-quoted,pip install "theoryforge[render]"as in the README, and the command pastes into zsh, bash, fish, PowerShell and cmd alike. A test fails on an extra outside double quotes in the README, the guides or the sources. It also fails on apip installline in a shell block that ends in a#comment, since an interactive zsh passes the comment on to pip.
Changed¶
- R's
tf_litmap()andtf_landscape()count pairs in linear time. R matched every new pair against all the pairs seen so far, so a record with 300 references took about a minute and a fetched corpus with references hours, against milliseconds in Python. The results are unchanged.landscape()no longer computes the co-citation map in either twin, since it never used it. R'stf_lit_diagram()builds its lines as whole vectors, where it copied every line written so far for each edge it added. 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 result gainsmax_token_share,field_tokensandphenomenon_tokensaftertheory_id. The three status names and the existing keys are unchanged, and so are the bundled corpus's statuses. API_SPEC section 15 pins the rules, and the landscape golden gains the new keys.litmap()andlandscape()give aUserWarningwhen 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. The text is the same in both twins (API_SPEC section 14).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), theseveritychart is titledPre-data riskiness, and the apps call the longer bars riskier claims, where they called them stronger tests. The values are unchanged. API_SPEC sections 9, 11, 12 and 20 and the goldens change with them.- The four prediction types are defined in the schema, API_SPEC section 4, the
add_predictiondocstring, the R help page and both methodology pages, and the bundled panic predictions now state the value and tolerance that theirpointtype claims. Five app examples typed a comparative, ordinal, anti-phase or invariance claim aspointorexistence, and now type itdirectional, with their declared severities unchanged. Their scores in the apps move with the labels. The rubric now gives each of the five 0.4, down from 1.0 for the three formerpointpredictions and up from 0.2 for the two formerexistenceones. Effort-recovery's precision falls from 0.667 to 0.333 (warn) and its aggregate from 86.9 to 83.6, happy-vowel's aggregate falls from 73.1 to 71.1 and cognitive-dissonance's from 76.7 to 73.3. validate(full=True)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.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 keys first and adds the evidence behind the verdict:articulated,underived,corroborated_new_registered,renamed,dropped,dropped_corroborated,content_lost,new_anomaliesandassumptions. The appraisal readsregisteredonly to report which corroborations were preregistered, and never gates on it. 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. API_SPEC section 10 states the rules, and the appraisal golden and the goldens of the amended example change with them.- 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 a null score, 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, afteraggregate_score. An empty theory scored 26.0 and now scores 0.0, and adding a prediction to it no longer lowers its score (18.0 became 1.2). Parsimony no longer penalises declared auxiliary assumptions. It fails only when an assumption added in response to an anomaly has no independent corroboration, by the appraisal's ad hoc rule, and it isn/awhen no assumption was added that way.parsimony_ratio_maxis removed. Non-redundancy follows the redundancy screen's flags, and the screen also flags a definition contained in another:redundancy_check()gainsoverlap, the overlap coefficient, and flags a pair whose overlap reachesredundancy_overlap_max(0.85) when both definitions hold at least three tokens. The deliberately redundant pair in the weak example (Jaccard 0.8, overlap 1.0) is now caught. Mean severity counts every prediction, using the claim-form rubric where none is declared, andadd_prediction()gainsseverity. Causal testability counts every directed relation,mediatesandmoderatesincluded, from a relation table that API_SPEC section 28 sets out. The bundled theories move from 84.8 to 87.3 (panic network), 87.7 to 89.8 (its amended version), 85.1 to 89.8 (modality switching) and 12.0 to 2.2 (the weak example), each with a coverage of 0.92. The app examples move from 73.3 to 80.4 (cognitive dissonance), 83.6 to 86.2 (effort and recovery), 71.1 to 74.6 (happy-vowel), 76.8 to 81.3 (planned behaviour), 80.6 to 83.0 (self-determination) and 68.6 to 78.5 (stereotype threat). API_SPEC sections 4 and 6 state the rules, and the report, dossier, rigour and roadmap goldens of the four fixtures change with them. 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.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. The apps shown/aand the coverage, count the items with nothing to assess and show the overlap of each construct pair.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 preregistration reports what the theory records about each hypothesis and what its
causal graph implies. The header gives the version, with its parent when one is named.
Under each hypothesis come the auxiliary assumptions that protect it, each
coreoradded for <id>, the registered rivals it discriminates and its test record, indented by the width of its number so that Markdown nests them. A new section lists the conditional independencies thatimplications()derives with its default settings, each as a code span so that Markdown keeps the_||_notation. When there are none, one line gives the reason: the refusal's text, no directed relation, no set of constructs that separates a pair the theory leaves unjoined or every pair adjacent. Before the provenance,dossier()adds a test record, which lists every outcome with its result, observation, date and registration. It also adds the evidence, with what each entry supports, its direction and its DOI. The independencies appear once in the dossier, in the appended preregistration. API_SPEC sections 11 and 20 pin the format. The fourprereg.mdand fourdossier.mdgoldens and their R copies change, and so does thepreregisterentry of every edge-case record that holds one. litmap()keys a reference that holds a DOI by the DOI, lower-cased and without its resolver prefix, asnew_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.landscape()reads no references, so its results are unchanged. The corpus schema now ships with the package, atimportlib.resources.files("theoryforge") / "schema" / "corpus.schema.json", andscripts/gen_golden.pywrites both packages' copies. API_SPEC section 14 states the rule, the edge casecorpus-doi-casepins it in both twins, and thecorpus-mixed-idsedge record now labels its DOI in lower case. The goldens are unchanged, since no reference in the golden corpora is a DOI.- Python packaging: the build backend is no longer held below hatchling 1.32, whose
metadata version twine now accepts. The wheel's core metadata moves from version 2.4 to
2.5, and the sdist and the wheel hold the same files as before. The licence was already
declared once, as the SPDX expression
MIT, without the licence classifier that PEP 639 deprecates. A test in the Python suite now fails if the cap or such a classifier returns.
Added¶
Theory.copy()returns an independent copy of a theory to amend.appraise_amendment()now refuses to compare a theory with itself, which the in-place builders made easy to do by accident and which always returnedneutral.litmap(corpus, min_link=2, min_cocitation=None)thresholds the co-citation map on its own (by default atmin_link), andlit_diagram(obj, type, max_edges=None)draws only themax_edgesstrongest edges of a keyword or co-citation diagram, ties broken by(a, b), with only their endpoints as nodes. Co-citation maps of real corpora run to thousands of edges: 200 OpenAlex records give 11,211 at a threshold of 2, against 616 keyword edges. The literature guides in both languages show both arguments.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 (UserWarning in Python,warning()in R, the same text) when its steps depart from the exact solution by more than 5 per cent. 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 regulation example in the workflow page swung more than six times too wide after 500 steps. The record gainsmethod,ignored(the propositions that couple nothing: moderates, associates and propositions with an endpoint that is not a declared construct) andopposed(the pairs whose increases and decreases offset each other), afterinit. The propagator is a degree-18 Taylor polynomial with scaling and squaring (Moler & Van Loan, 2003), written in explicit loops so the two languages give the same bits. API_SPEC section 22 pins the algorithm, the warning and the new keys. The foursimulate.jsongoldens gain the keys with unchanged trajectories, foursimulate_exact.jsongoldens are added, and the edge-case records gain the keys. The apps default to the exact method, offer a method selector, show the Euler warning and tell an unstable system from a coarse Euler step.fetch_corpus(query, per_page=25, mailto=None, api_key=None, max_records=None)accepts an OpenAlex API key, by default theOPENALEX_API_KEYenvironment variable, and sends it only as anAuthorization: Bearerheader.max_recordspages through OpenAlex's cursor beyond one page. The corpus gains a top-levelsource(service, endpoint, query, UTC retrieval time, total matches, records kept, page size and order) and each record itsdoi, both optional properties inschema/corpus.schema.json.litmap()andlandscape()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, where connected components give one theme.litmap()also gainsmin_theme_size,max_theme_sizeandmax_df, which excludes keywords shared by most records and reports them asfield_terms. The full signature islitmap(corpus, min_link=2, method="components", min_cocitation=None, min_theme_size=2, max_theme_size=10, max_df=1.0), so amin_cocitationpassed by position must now be named. API_SPEC section 14 pins every rule, tie-break and summation order, and both twins give the same floats. Components remain the default for this release, the components record is unchanged and the giant-theme warning now 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,
fixtures/openalex-panic-2026.corpus.yaml, ships in both packages (example_path("openalex-panic-2026.corpus.yaml")), with five new goldens built on it and on the demo corpus. It was made by replaying one OpenAlex response saved on 2026-10-01 throughfetch_corpus, keepssource, drops the references and is offered under CC0, as OpenAlex's data are. osf_push(overwrite=False). OSF storage answers 409 to a second upload under the same filename, so depositing a theory again failed in both twins with no way round it.overwrite=Truelists the project folder first and sends the dossier to the existing file's upload link, WaterButler's update route, which records a new OSF version. When no file of that name exists, the file is created as usual. The dry run then shows the lookup request too.OSFUploadErroris exported.implications(theory, cycles="refuse")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 graph is replaced by its acyclification and read by m-separation there, so the record names the criterion"sigma", says inacyclicwhether the graph has a cycle and lists the feedback loops infeedback. The bundled panic network implies that arousal and avoidance are independent given perceived threat. The default still refuses a cyclic graph, and its message now ends; set cycles to 'sigma' to derive sigma-separation statements. Any other value ofcyclesraisesimplications requires cycles to be 'refuse' or 'sigma'. Four goldens,<id>.implications.json, hold the sigma record of each fixture, and API_SPEC section 27 pins the acyclification and the order of the loops.post_data_severity()(alsoTheory.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 ofPhi((estimate - lower) / se)andPhi((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.validate(full=True)checks the new fields, anddossier()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. API_SPEC section 29 pins the rule and its refusals, and a new golden,modality-switching-2026.post_data_severity.json, holds the example's record.- Predictions accept an optional
testblock recording the planned sample size, alpha, smallest effect of interest and power, andpreregister()lists it in a Sampling plan section. The block is recorded, not scored: power does not change a prediction's severity.add_prediction()gainstest, andvalidate(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. API_SPEC section 30 pins the block and the line.
Deprecated¶
simulate()'s defaultmethod="euler". The default will change to"exact"in the next minor release. Passmethod="euler"to keep the current trajectories.
Documentation¶
- The CRAN DOI and the Zenodo DOI are shown as full links on the two sites and in the
READMEs. The Python home page gives the Zenodo concept DOI,
https://doi.org/10.5281/zenodo.21229964, in place of the Zenodo badge, and the About
pages no longer hang the DOI's link on the word Zenodo. The Python BibTeX entry's
urlgives the PyPI page, so a style that prints both fields no longer shows the DOI twice. The R package's README gains a badge for the DOI CRAN assigned, which the R home page leaves out of its sidebar, where the DOI is already given under Citation. - Both methodology pages, the docstring and the R help page state what the simulation
leaves out: one gain for every coupling,
causesandmediatestaken as positive,moderatesandassociatescoupling nothing,functional_formnot read and a common initial value. Being linear, it cannot show bistability (Robinaugh et al., 2024). An acyclic theory decays at the damping rate only asymptotically, and the regime of a theory with loops is set by the gain against the damping. The workflow page's simulation example runs for 20 time units withmethod="exact"and shows the oscillation its five Euler steps hid. - The literature page'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, and in Python it failed on a
missing year and on the
Int64years of a resumed checkpoint. The recipe is now a function,scopus_corpus_to_tf(), shown in full in both languages and run by both test suites on a stand-in corpus. - The landscape statuses are described as what they are. Under-theorised means that none of the registered accounts addresses a theme, and crowded that two or more do, which calls for predictions that discriminate between them. The methodology and literature pages, the READMEs and the apps called a crowded theme a redundancy risk, and the apps one where a new theory would add little. The literature pages and the apps' help say that connected components put nearly every keyword of a real corpus in one theme, and the literature pages show the warning. The R article's example theory is titled after its phenomenon, since a title naming its constructs now leaves them unmatched.
- The workflow pages in both languages say what a repeated OSF deposit does and how
overwriteadds a new version. - CONTRIBUTING installs the docs extra needed by
mkdocs build. - The schema documents
risk_scoreandseverity_at_testas informational fields that no function reads. - The
fetch_corpusdocumentation and both literature guides describe API keys in place of the retired polite pool (OpenAlex ignoresmailto), give the cost of a page and the daily budgets, and explain that OpenAlex keywords have been written by a language model since late September 2026 and change over time, while the concepts fallback brings a deprecated vocabulary with capitalised names. Their examples ask for pages of 100 works, the largest page OpenAlex supports now that it has deprecated 200. - The schema, the
add_assumption()docstring and the R help page defineadded_foras the id of the prediction whose anomaly an assumption answers, and the schema describesprotectsandregistered. The methodology pages and the apps no longer attribute theneutralverdict to Lakatos, whose scheme has only progressive and degenerating problemshifts. The bundled examples and the app examples record the builder names in their provenance (tf_add_construct,tf_add_proposition,tf_add_prediction). - Both methodology pages quote the checklist's criteria and correct the causal
testability row, which described an export to a DAG with derivable implications. They
call the five thresholds the package's defaults where they called them calibrated, and
no longer say that a theory passing every item scores 100: the panic example passed all
twelve at 84.8. They explain that the aggregate is compensatory, so a blocked theory can
outscore one whose gate passes, and that items can pass below a score of 1. They no
longer say that a field can override the weights in its own copy of the checklist: the
weights and thresholds are fixed, every report records the checklist version, and
recombining the per-item scores gives a result not comparable with the default
aggregate. They add that the lexical screen cannot detect empirical redundancy
(Le et al., 2010; Rönkkö & Cho, 2022), that a rival named in
diagnostic_vsis declared and not verified, which holds for the rubric's 0.1 bonus as well, and that the derivation chain checks references only. The checklist's criteria say the same, and its comments call the thresholds defaults. The pages also note 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. The schema describes a prediction'sseverityas a declared pre-data severity,redundancy_check()gives its references' full titles and the getting-started page shows both measures of the redundancy screen. - 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). Theimplicationsdocstring, the R help page, both workflow pages and the README warn that a conditional implication tested on fallible measures is rejected too often. With standardised paths of .5 and a conditioning construct measured at a reliability of .8, a true statement is rejected in about 14, 29 and 51 per cent of studies of 200, 500 and 1,000 observations, because the error leaves residual dependence (Westfall & Yarkoni, 2016). They point to tests in a latent-variable model (Thoemmes et al., 2018). The statements are called the conditional independencies the causal graph implies, and a basis set only for a graph of directed relations alone. - Both workflow pages show the panic network's sigma-separation statement together with
the assumption it rests on, a single equilibrium for each feedback loop, which a theory
of alternative stable states violates. The methodology pages, the README and API_SPEC
sections 5 and 27 describe the
cyclesoption, and the README no longer quotes a count of golden artefacts. - The R article on developing and testing a theory demonstrates
compile_sem()on the modality-switching theory and no longer says that the panic network's syntax can be passed directly tolavaan::sem(). The apps no longer say that the syntax can be pasted straight into a fit. The panic network's feedback loop leaves its model unidentified. Thecompile_sem()docstring, which the API page renders, the R help page, the apps and API_SPEC section 19 call the syntax a starting point for a fit. Indicator names are the sanitised measurement entries, so entries written as data column names give syntax that fits as it stands. 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. - The README no longer says that construct non-redundancy is checked against the field, and the API page no longer says that the redundancy screen compares constructs against it. The screen compares the definitions within one theory. The README now says that the packages map a theory and its registered alternatives onto the themes of a literature corpus.
- The quick start on the PyPI page needed a clone of the repository. It now reads the
bundled examples through
example_path(), so it runs straight afterpip install, and CI runs it against the installed wheel from outside the repository. The page's logo has an absolute address, since PyPI showed the relative one as a broken image. - The 0.6.0 entry gave 2026-08-21, a day on which no release appeared, and is now dated 2026-08-27, the day 0.6.0 was released on GitHub and PyPI. It also records that CRAN published the R package's 0.6.0 on 2026-09-01. The 0.1.0 entry no longer calls 0.1.0 a CRAN release, since the R package first reached CRAN with 0.6.0.
- The R article on developing and testing a theory and the Python workflow page call the
preregistration the hypotheses section of a preregistration, to be completed with the
design, sampling plan, analysis plan and inference criteria that a template such as
PRP-QUANT asks for (Bosnjak et al., 2022). The article called it suitable for deposit
and the document a registry would receive. The
preregister()anddossier()docstrings, the R help pages and both pages describe the new lines and sections. - Every example on the Python site runs as shown, the diagrams are rendered at build
time (two pasted figures had drifted from their code) and the methodology table quotes
the checklist itself. The workflow and literature guides now read the bundled examples
through
example_path(). Their visible code read afixturesdirectory that a hidden block found in a clone of the repository, so a reader's copy failed with aNameError. The home page read its examples from the working directory. The eight Graphviz figures are drawn byrender_diagram()when the site is built, so the docs workflow installs Graphviz and the render extra. The pasted context figure lacked two of the panic network's five scope conditions, and the development roadmap still showed the score, numbering and criteria of an earlier checklist. Both methodology pages read the criterion column of their table from the checklist the package ships, and a check that the checklist gains without a name on the page stops the build. The docstring oftheoryforge.examplesand the getting-started page nametf_example_path()andtf_example_names()as the R counterparts, and CONTRIBUTING says that a local docs build needs the Graphviz binaries. A test runs each guide's visible code from an empty directory. - The schema compiles under strict JSON Schema validators and no longer promises version
gating, draft-stage downgrades or form profiles that the packages do not implement. Its
note on the closed top level was a
commentkeyword, which JSON Schema does not define and Ajv's default strict mode refuses, and is now a$comment. Theschema_versiondescription says thatvalidate()requires the field andvalidate(full=True)checks its pattern, and that neither package gates loading on it. Thematuritydescription says that at draft maturity failed blockers are still reported as failing and still count in the score, and only the gate is advisory. Thetheory_formdescription says that the checklist applies the same twelve items to every form. API_SPEC no longer lists implemented functions as stubs, and its diagram section names all ten types. - Apps: the apps' README records the pinned webR and Pyodide versions and notes that the R app's jsonlite and yaml are not pinned. The R app's browser console records the versions each session installs.
Internal¶
- Python's two copies of the nonempty-string test (
core._nonempty_strandrigor._ne_str) and the per-module_listhelpers are merged into_access.py. - The schema and rigour checklist copies that each package ships are now written only by
scripts/gen_golden.py, CI fails when they drift, and the webR app vendors the copy the R package ships. The parity check no longer treatstrueas 1 or unboxes nested one-element arrays, checks the key order of the rigour report, compares the six app examples across the two engines and records the outcome of a corpus of malformed theories (fixtures/edge/). - The parity check gains a round-trip phase. Python writes every fixture, app example and
readable edge case to YAML and JSON, and R reads each file and writes it again. Every
file must then read back as the theory Python first read, a one-element array still an
array, and, when jsonschema is installed, still validate against the schema. New edge
cases cover the reading rules, with construct ids
yandn, a merge key, unquoted dates, byte-order marks, a missing final newline, repeated keys in YAML and JSON, awkward scalars and an empty sequence. scripts/gen_fold_table.pywrites the fold tableschema/fold.jsonfrom Python'sunicodedata, once, so neither twin depends on its runtime's Unicode tables, and a test fails when the committed table differs from what the script writes.scripts/gen_golden.pymirrors it into both packages with the schema and checklist, and now does so before it computes the goldens, so they come from the files the packages ship. The webR app vendors it. Eight edge cases cover whitespace outside ASCII, the Turkish dotted capital I, Greek and Cyrillic case, decomposed Cyrillic and Chinese text, and the DOI golden's candidate list gains the spellings now recognised, with an unchanged result.- The edge phase of the parity check also covers literature corpora
(
fixtures/edge/<name>.corpus.yaml|json), recording whatread_corpusandlitmapmake of each._access.as_list, which only the literature layer used, is replaced by the corpus checks inlit.py. - The
devextra installs jsonschema 4.0 or later fortests/test_schema_agreement.py. The test checksvalidate(full=True)against a validator for the schema's own draft, 2020-12, on the shipped theories, thegapsfiles infixtures/edge/and one-fault variants of a theory that uses every field. It skips itself under an older jsonschema, and the CI job that installs the declared minimum versions runs it under jsonschema 4.0. Nine new edge cases pin the full pass in both twins: the eight violations it used to miss, a value of the wrong type in every optional field, a quotedpassed, four formal-model types outside the enum, an extra version key and aschema_versionofone. - The OpenAlex adapter sends every request through one replaceable call in each twin:
lit._urlopenin Python, and in R.tf_http(method, url, headers, body), which returns the status and body for.tf_http_check()to judge. The R tests replace it withtestthat::local_mocked_bindings(), sotestthat (>= 3.1.7)is now in Suggests. - The relation table of API_SPEC section 28 is defined once in each twin (
_relations.py,relations.R), and both suites check it against the schema's enum. The checklist's parsimony item and the amendment appraisal share one ad hoc rule (_status.classify_auxiliary,.tf_classify_auxiliary), and the non_redundancy item reads the redundancy screen's own flags. The edge-case records gain the report'scoverage, and a new edge case covers an assumption corroborated beyond its anomaly, a definition contained in another and a theory whose only relation ismediates. - CI's apps job compiles both schemas with Ajv in its default strict mode (ajv-cli
5.0.0), which python-jsonschema cannot stand in for, since it accepts an unknown
keyword as an annotation.
tests/test_schema_document.pyand its R twin check that every key of the schemas the packages ship is a JSON Schema keyword, and hold the three rewritten descriptions to the behaviour they describe. - Apps: no CI job ran the glue code each app runs in its engine, so a renamed internal or
a file left out of a manifest would have broken the deployed app alone.
scripts/app_smoke.pyandscripts/app_smoke.Rcopy the files each manifest lists into a temporary directory and run the glue there with only the packages the browser has, PyYAML in Python and jsonlite and yaml in R, out of reach of an installed theoryforge. Each calls every operation on every example, once for each option of a select parameter, and checks that an invalid theory is reported as invalid. The CI apps job runs both, and the docs workflow runs them on the staged site before it deploys. A Node test fails whenscripts/app_smoke_cases.jsonand the operations the apps offer differ. The R app loads webR 0.6.0, pinned as Pyodide 0.27.7 is. It loaded thelatestaddress, which serves webR's development build. - The R package's spelling test fails a check run with
NOT_CRANset, as indevtools::check()and on every CI leg, when a word is in neither the British English dictionary norinst/WORDLIST. It used to list such words and carry on, so every R check on CI 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, and the contributing guide says how the test is run and what to do when it stops.
0.6.0 - 2026-08-27¶
The R package reached CRAN with this version (published 2026-09-01).
Fixed¶
- The two engines wrote different bytes on Windows. Every file the package writes now goes through a single LF-only, UTF-8 writer in each language, so the R half no longer emits CRLF where the Python half emits LF. The byte-identity claim rests on that writer, and nothing else in the contract changed.
- A misspelt top-level key silently changed the score:
predicitions:was dropped without a word, taking its whole collection with it and moving the rigour score.validate()now refuses an unknown top-level field in both languages, with identical message text, and the schema'sadditionalPropertieswas set to match so a third-party validator agrees with ours. - R scored input that Python refused.
tf_readandtf_read_corpusaccepted a top-level YAML sequence of mappings, and both now reject it. - A mistyped enum value, such as
theory_form: [network], took the two validators in opposite directions. R's%in%unboxed the one-element sequence and let the file through, while Python reachedx in <set>on an unhashable value and abandoned the errors it had collected for a raw TypeError. Both engines now test for a nonempty string before testing for membership, and a collection entry that is not a mapping, as inconstructs: [arousal, threat], has each of its required fields reported missing, where Python used to raise an AttributeError. API_SPEC section 2 records the rule. simulate()accepted duplicate construct ids, producing two different but equally plausible trajectories from one file. Both languages now refuse them.- A failed
quarto renderreturned its output path as though it had succeeded, andtf_writedid not force UTF-8 as its sibling writers do. Both corrected. - A string severity validated in both engines, then crashed one and scored in the other.
The schema types a prediction's
severityas a number in [0, 1], butvalidate(full)never checked it: Python'scheck()died with a raw TypeError while R silently coerced the string and produced a verdict. Full validation now enforces the typed range, and the scorer refuses a non-numeric severity with the same named error in both languages, socheck()withoutvalidate()cannot diverge either. - Unequal-length embedding vectors produced two confident wrong answers. R recycled the
shorter vector and Python silently truncated the longer, so the same embedder could
report two different similarities for one construct pair.
embedding_redundancynow refuses the pair in both languages, naming the constructs and the lengths. - The Python OpenAlex adapter lost its concepts fallback. A keywords list whose entries
all carry a null
display_nameis non-empty before filtering, so the fallback to the top concepts never fired and the record ended up with no keywords at all. Nulls are now filtered before the emptiness test, as R always did. - Null fields rendered differently across the twins. Python's rigour report emitted
nullfor a nullidormaturitywhere R emitted"", breaking the semantic comparison, and a null theory id turned the OSF deposit filename intoNone.dossier.mdwhere R fell back totheory.dossier.md. Python now reads nulls as empty strings, matching R. scripts/reproduce_all.ps1pointed rmarkdown at a Quarto pandoc directory that may not exist on the machine. The variable is now set only when the directory is present, matching the bash twin's restraint.
Changed¶
- The rigour report and the dossier record the checklist version that produced them, so two reports written against different checklist revisions are no longer silently comparable. The package version is deliberately not recorded: these artefacts are parity-tested across the twins, and a per-language release number would break that. The reasoning is written into API_SPEC section 4.
simulate()echoes back k, damping and init as well as dt and steps, 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. Moving the verdicts of a scored item would have been
the larger change, so the criterion and the methodology pages 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
acyclicity check the criterion once implied now lives in
implications(), which can refuse a graph outright instead of quietly rescoring it. - The R 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.
Added¶
- A theory's testable implications are now derived. The
rigour checklist cited the derivability of a causal theory's implications and the
methodology pages said the causal relations export to a DAG with derivable
implications, while the export was emitted as written and nothing read it.
implications()in Python andtf_implications()in R read the causal propositions as a directed graph, check it for acyclicity, and return 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 (Pearl, 1988; Shipley, 2000), rendered in the notation dagitty prints. The set is the shortest complete statement of what the theory forbids in data. A cyclic graph has no basis set and is refused with the cycle named, which is what both panic-network fixtures get. A theory with no causal relations comes back with an empty set and no error. The derived sets were checked againstdagittyandggm, two published implementations, which join the R package's Suggests for that purpose and whose tests skip when they are absent. - A fourth example theory ships with both packages, and it is the worked example for
implications(). Both panic-network fixtures are cyclic, so until now every bundled theory demonstrated only what the function refuses.fixtures/modality-switching.theory.yamlstates 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 carrying both a fork and a collider, and a basis set of six conditional independencies, checked statement for statement againstggm::basiSetand confirmed bydagitty::dseparated. It passes full validation in both engines and the whole rigour checklist. The panic fixtures are kept: a feedback loop is legitimate theory, and the refusal is worth demonstrating too, so the Workflow modes page and the Developing and testing article now show both outcomes. The golden tree grows from 55 artefacts to 71 with it. - Both packages ship the example theories and the demonstration corpus. Python reaches
them with
example_path()andexample_names(), R withtf_example_path()andtf_example_names(), and both list the same.yamlfiles in the same order. The README's R quick start now uses a shipped fixture, so it works straight afterremotes::install_githubwith no clone. - CI now exercises the wheel a user would install, where it used to exercise only the
checkout. The Python matrix gains
3.14, and the classifiers now advertise it. A new job installs the built wheel
into a bare environment outside the repository and resolves the packaged JSON
schemas from there through
importlib.resources: theoryforge ships those schemas inside the package, and an editable install would find them in the checkout even if the wheel omitted them entirely. A second new job installs every declared minimum dependency floor, PyYAML and the two extras alike, and a further job runs the R suite on the declared R 4.1 minimum, which the six-cell--as-cranmatrix sits well above and so never exercised. A weekly schedule runs the suite when nobody has pushed, so upstream drift shows up as a dated red badge before it can catch the next release. - The golden gate covers every duplicated tree. It watched the root copy only and
reported modifications alone, so a golden the generator newly created or deleted
slipped through, as did any drift in the copies shipped inside the two packages.
Relatedly,
reproduce_allverified nothing: it rewrote the goldens it was meant to check, so it could never fail. It now verifies, and gates on mypy as CI does.publish.ymlgained the least-privilege token scope its siblings already declared. - The R package's spelling word list is read at last.
inst/WORDLISTshipped with nothing consulting it, sospellingjoins Suggests and atests/spelling.Rruns the check underR CMD check, as the sibling packages in the family do.
Documentation¶
- The Python API reference gains the Packaged examples group (
example_names,example_path), which the R reference index already carried, so the two indexes list the same functions in the same groups. The mkdocs build comment now installs thedocsextra, whose dependencies include themarkdown-execplugin the previously quoted command omitted. - The changelog's reference links now cover every released version. The definitions had stopped at v0.2.0, leaving the newer version headings as dead bracket text on the rendered changelog page.
0.5.0 - 2026-07-23¶
Changed¶
- 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 running down the blockers and the advisory items set three abreast. - The three SVG chart views (
venn,rigour,severity) now declare awidthand aheightalongside theirviewBox. Lacking an intrinsic size, each chart was previously stretched to the width of its container, and because the three views have different natural widths the same declared 13px label came out at a different size in each figure. Each view now renders at scale 1 wherever it is embedded. - The
venndiscs take the construct-border teal for their outline in place of the former navy. The outline is what carries the set structure, and the navy fell below the 3:1 contrast floor for graphical objects on a dark page, which left the figure close to invisible under the dark theme. - The bundled
panic-networkfixtures give the three constructs distinct boundary conditions, and declare all of those conditions at theory level. Thevennview drawn from the previous values put a zero in six of its seven regions, so the figure showed nothing about where construct scopes diverge.
All of the above are mirrored byte for byte across R and Python, and the goldens,
the tests and the specification (API_SPEC.md) are updated with them.
Documentation¶
- The R Get started vignette shows what
tf_validate()returns and demonstrates the failure path, which the prose previously only described. - The R development article runs
tf_osf_push()in its default dry-run mode, so the planned deposit appears on the page, where it was previously withheld as a network call. - The Python literature page marks its corpus listing as an excerpt of the
eight-record fixture, pairs each
lit_diagramcall with its own output, and renders the keyword co-occurrence and theme landscape views as figures. - The Python simulation example prints every step of the trajectory it discusses, and the surrounding prose now matches the printed numbers.
- The chart figures on the Python guides are generated when the site is built, no longer pasted in, so they cannot drift from the library.
0.4.0 - 2026-07-16¶
Changed¶
- The DOT diagram views are redesigned for content and legibility, identically in both packages. Every view now opens with a shared Meridian style prelude (Helvetica type, role-coloured rounded nodes: teal constructs, amber propositions, navy predictions, green/red outcomes, paper scopes, grey rivals); labels are word-wrapped so nodes stay narrow; workflow and pipeline nodes carry the id together with the relation or type, where a bare word stood before; the development roadmap chains its items into a single column, in place of an ever-wider row; and the theme landscape colours themes by status. Every view now fits a documentation column without horizontal scrolling. The IR remains byte-identical across R and Python; goldens, tests and the specification are updated (API_SPEC.md).
Documentation¶
- The pages that print or render the diagram views show the new output, and the remaining code blocks without visible results (the literature article's OpenAlex fetch and scopusflow hand-off, and the Python workflow page's provenance, report, preregistration and dossier) now show them.
0.3.0 - 2026-07-15¶
Added¶
- Native diagram rendering in both packages, tailored to each language and
layered on the unchanged, byte-identical IR. R gains
tf_render_diagram()(DiagrammeR widget, or a standalone SVG string withas = "svg"; packages in Suggests), and Python gainsrender_diagram()andTheory.render_diagram()(agraphviz.Source, via the optionaltheoryforge[render]extra). Both accept a theory or a raw IR string, so literature diagrams render the same way; the three SVG chart views pass through;causal_dagis refused with a pointer to dagitty. Rendering is parity-exempt (API_SPEC.mdsection 26).
Documentation¶
- The digraph views now render as figures on both documentation sites, following the code that produces them.
0.2.0 - 2026-07-15¶
Changed¶
- The severity chart is re-laid out: bars now start just past the longest row label, and each
value trails its own bar. This changes the diagram intermediate representation for
type = "severity", and R and Python remain byte-identical.
Documentation¶
- The documentation now shows the
provenance,development_roadmap,pipelineandco_citationviews, the embedding-redundancy screen,validate(full = TRUE)and the remaining build verbs, and it gains a section on rendering and depositing.
0.1.0 - 2026-07-10¶
This is the first public release: a rigorous, reproducible workflow for building, developing and testing scientific theories, delivered as feature-parity R and Python (PyPI) packages. The R package reached CRAN with 0.6.0.
Core (P0)¶
theory.schema.json+rigor_checklist.yamlas the shared, versioned source of truth, with API_SPEC.md pinning edge-case behaviour, including the severity chart's 15-character id truncation rule, the scalar-singleton array reading and the OSF filename encoding.- Theory-object I/O and structural validation (
read/write/validate). Where the schema expects an array of strings, a nonempty scalar string is read as a singleton list in both packages (API_SPEC.md section 4), so natural YAML such asderives_from: p1yields the same rigour verdict, gate and validation outcome in R and Python; an empty or whitespace-only scalar counts as absent, and cross-language regression tests cover the rule. - The 12-item machine-checkable rigour checklist with weighted aggregate score and a blocker gate (
check/report). - Diagram intermediate representations: nomological net, provenance, causal DAG (
diagram). - Deterministic lexical construct-redundancy screen (
redundancy_check).
Workflow modes (P1)¶
- BUILDING: a fluent builder API with auto-logged provenance (
new_theory/tf_theory+add_*/tf_add_*). - TESTING: an operationalised severity rubric (
severity) and preregistration export (preregister). - DEVELOPMENT: Lakatosian progressive/degenerating amendment appraisal (
appraise_amendment). - Two further diagrams: development roadmap and hypothesis→tested-theory pipeline.
- A
draftmaturity state that runs the checklist in advisory (non-blocking) mode.
Bibliometric layer (P2)¶
read_corpus,litmap(keyword co-occurrence, deterministic connected-component themes, co-citation).landscape: maps a theory and its alternatives onto themes, flagging under-theorised fronts and redundancy risk.lit_diagram(keyword co-occurrence, co-citation, theme landscape) and a parity-exempt OpenAlexfetch_corpusadapter.lit_diagram/tf_lit_diagramlist the valid types in the unknown-type error, matchingdiagram/tf_diagram.new_evidence_dois: a deterministic, dependency-free check for candidate DOIs not yet cited by a theory's evidence or alternatives, so a search from any external tool, including the companionscopusflow/scopusflow-pypackages, can be checked against what the theory already engages with.
SEM compilation and audit bundle (P3)¶
compile_sem: compile constructs + propositions to lavaan model syntax.dossier: a reviewer-facing Markdown audit bundle (rigour report + severity + provenance + preregistration).
Simulation, reporting & adapters (P4)¶
simulate: a deterministic dynamical-system runner derived from the construct network (parity-tested trajectories).render_report: a Quarto report wrapping the deterministic audit dossier.embedding_redundancy: an opt-in, parity-exempt embedding screen (pluggable embedder), complementing the default lexical screen.osf_push: an OSF deposit adapter (dry-run by default, with a live upload requiring the user's token).osf_push/tf_osf_pushpercent-encode the filename component of the OSF upload URL, keeping the dry-run request dicts identical across languages.
Visualisation and references¶
- Ten diagram views via
diagram/tf_diagram: nomological net, provenance, causal DAG, development roadmap, pipeline, and the newcontext(the theory, its scope and its rivals),workflow(the building-to-testing pipeline),venn(construct scope overlap),rigour(the checklist as a colour-coded status grid), andseverity(per-prediction severity bars). The last three are returned as SVG. - A "Methodological foundations" documentation page that cites the verified literature behind each rigour item, with DOIs. The machine-readable BibTeX ships with the R package at
inst/REFERENCES.bib. The risk-severity item's citation was corrected after a Crossref re-audit (Cohen, 1992 removed as not supporting prediction severity).
Quality & reproducibility¶
- Cross-language parity enforced over 55 golden artefacts in CI, with byte-identical diagrams (DOT and SVG), markdown, and lavaan outputs and semantically-equal JSON. The
panic-network-2026.new_evidence_dois.jsongolden is vendored with the R package and exercised by its tests. - The R literature layer and amendment appraisal sort with radix (codepoint) ordering regardless of locale, matching Python for mixed-case keywords and ids. A mixed-case parity test runs in both suites.
- Both packages read their version from package metadata, with no copy duplicated in source: Python
__version__comes from the installed distribution's metadata, and the R citation (inst/CITATIONand the About article) from the package metadata. - R passes
R CMD check --as-cranwith 0 errors and 0 warnings (1 note, the standard new-submission note). Python builds a wheel and sdist passingtwine check, is ruff- and mypy-clean, and shipspy.typed. - Test suites: Python (pytest) and R (testthat), plus a dedicated parity job.
Not yet implemented¶
- A live OSF upload requires the user's own token.
osf_pushships with a dry-run default. - Richer (nonlinear / agent-based) computational-model runners, and built-in embedding-model integrations beyond the pluggable
embedding_redundancyinterface.