---
title: "Building, checking and diagramming a theory"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Building, checking and diagramming a theory}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = FALSE, comment = "")
# Console colour carries no meaning on a rendered page. pkgdown turns it on for
# its own build, and the escape sequences then reach the reader as literal text,
# so colour is switched off here for a plain vignette render and a site build
# alike. The fixed width keeps printed output inside the documentation column.
options(cli.num_colors = 1, cli.hyperlink = FALSE, crayon.enabled = FALSE,
        width = 80)
# Both optional packages are needed to turn the DOT source into an inline
# SVG, so the rendering chunks below are skipped when either is absent.
has_diagram <- requireNamespace("DiagrammeR", quietly = TRUE) &&
  requireNamespace("DiagrammeRsvg", quietly = TRUE)
```

`theoryforge` treats a scientific theory as a versioned, machine-checkable
object. This vignette walks through the core loop of building a theory,
checking its rigour and diagramming it (see the
[package overview](https://pablobernabeu.github.io/theoryforge/r/) for the
R/Python parity guarantee).

```{r setup}
library(theoryforge)
```

## Building a theory

A theory is built incrementally with the BUILDING-mode verbs. Each verb appends
to the theory and records a provenance entry, and each returns the theory, so
the steps compose with the native pipe.

```{r build}
theory <- tf_theory("panic-network", "A network theory of panic") |>
  tf_add_construct("c_arousal", "Physiological arousal",
                   "Bodily activation in response to a stressor.",
                   measurement = "heart rate variability",
                   boundary_conditions = "awake adults") |>
  tf_add_construct("c_threat", "Perceived threat",
                   "Appraised danger of bodily sensations.",
                   measurement = "self-report appraisal scale",
                   boundary_conditions = "awake adults") |>
  tf_add_proposition(
    "p1", "c_arousal", "c_threat", "causes",
    mechanism = "Activation raises the salience of threat cues."
  ) |>
  tf_add_prediction("h1", "Arousal raises threat appraisal by a fixed amount.",
                    "point", derives_from = "p1")

isTRUE(tf_validate(theory)) # structural checks: required fields and enums
# also checks referential integrity of ids and cross-references
isTRUE(tf_validate(theory, full = TRUE))
```

`tf_validate()` returns `TRUE` invisibly on success and stops with a message
listing every problem otherwise, which is why the calls above are wrapped in
`isTRUE()` to show the result. With `full = TRUE` it additionally checks
referential integrity, that ids are unique and every cross-reference points to a
declared id. It does not require any optional dependency.

The failure path is the more informative one. Pointing a prediction at a
proposition that was never declared leaves the structural pass untouched, since
every required field is still present and well formed, so only `full = TRUE`
reports the problem.

```{r validate-failure, error = TRUE}
broken <- theory
broken$predictions[[1]]$derives_from <- "p_missing"
tf_validate(broken, full = TRUE)
```

The building vocabulary has two further verbs. `tf_add_assumption()` records an
auxiliary assumption, and `tf_set_formal_model()` attaches a formal-model
reference. Applied to a copy of the theory, they leave the original untouched.

```{r build-more}
extended <- theory |>
  tf_add_assumption("a1", "Arousal is measured at rest.", added_for = "h1") |>
  tf_set_formal_model("sem", spec_ref = "panic-sem.lavaan")
```

Every verb appends a provenance entry, recording the step, the action and its
detail, so the record of how a theory was built travels with the object.

```{r provenance-entries}
do.call(rbind, lapply(extended$provenance, as.data.frame))
```

A theory can be written to and read back from disk. The format follows the file
extension (`.json` for JSON, otherwise YAML).

```{r io}
path <- tempfile(fileext = ".yaml")
tf_write(theory, path)
roundtrip <- tf_read(path)
identical(roundtrip$id, theory$id)
```

## Checking rigour

`tf_check()` runs the 12-item rigour checklist defined in the vendored
`rigor_checklist.yaml`.

```{r check}
report <- tf_check(theory)

report$aggregate_score   # weighted score, 0-100, rounded to 1 dp
report$gate              # "pass", "blocked", or "advisory" (draft maturity)
report$n_blockers_failed # count of failed blocker items

# Per-item detail (checklist order preserved):
report$items[[1]]$id      # "falsifiability"
report$items[[1]]$status  # "pass" / "warn" / "fail"
report$items[[1]]$score   # numeric in [0, 1]
```

Render it as JSON (valid, pretty-printed) or as an HTML fragment. The JSON
string is an artefact in its own right, shown here verbatim:

```{r report-json}
cat(tf_report(theory, format = "json"))
```

The HTML fragment drops straight into a page and renders as a real table:

```{r report-html, results = "asis"}
cat(tf_report(theory, format = "html"))
```

### Screening for redundant constructs

The lexical redundancy screen reports the Jaccard similarity of every pair of
construct definitions, sorted by descending similarity.

```{r redundancy}
tf_redundancy_check(theory)
```

The lexical screen is deterministic but shallow. When a semantic comparison is
wanted, `tf_embedding_redundancy()` takes any embedder that maps a definition to
a numeric vector and reports the cosine similarity of each construct pair. It
needs no optional dependency, since the embedder is supplied by the caller. A
toy bag-of-words embedder stands in for a real language model here.

```{r embedding-redundancy}
vocab <- c("bodily", "activation", "appraised", "danger", "salience")
embedder <- function(def) {
  words <- strsplit(tolower(def), "[^a-z]+")[[1]]
  vapply(vocab, function(w) sum(words == w), numeric(1))
}
tf_embedding_redundancy(theory, embedder)
```

## Diagramming

`tf_diagram()` emits deterministic intermediate representations for several
diagram types. The digraphs are Graphviz DOT, and the causal DAG uses dagitty
syntax.

```{r diagram}
cat(tf_diagram(theory, type = "nomological_net"))
cat(tf_diagram(theory, type = "causal_dag"))
```

The DOT strings render with any Graphviz tool, and `tf_render_diagram()` does
this without leaving R: it returns a DiagrammeR widget for the viewer and for
R Markdown, or a standalone SVG string with `as = "svg"`. The packages it uses
are optional, in Suggests, so the deterministic core stays dependency-free.
Rendered, the nomological net above reads as a figure.

```{r diagram-render, results = "asis", eval = has_diagram}
cat(
  '<div class="tf-figure tf-diagram">',
  tf_render_diagram(theory, "nomological_net", as = "svg"),
  '</div>',
  sep = ""
)
```

The causal DAG is the one view this does not cover, since it emits dagitty
syntax rather than DOT. Paste it into a dagitty tool such as
[dagitty.net](https://dagitty.net) instead. `tf_implications()` reads the same
subgraph without leaving R, checking it for acyclicity and returning the
conditional independencies it entails, which the Developing and testing article
works through.

## Simulation

`tf_simulate()` treats each construct as a state variable and integrates the
signed proposition network as a linear dynamical system. The trajectory is
deterministic:

```{r simulate}
sim <- tf_simulate(theory, steps = 5)
unlist(sim$states)
unlist(sim$trajectory[[1]]) # the common initial state
unlist(sim$trajectory[[6]]) # after five Euler steps
```

The two states separate immediately: arousal, which receives no incoming
coupling, decays under the damping term, while perceived threat is pushed up
by the positive coupling from arousal before the decay eventually takes over.
