Readme: document corpusStats, textMetadata and collocationScoreQuery

These three are exported, useful and were shown nowhere in the Readme,
which not only leaves readers to find them in the reference
documentation, but effectively hides them from an LLM whose context is
the Readme: it cannot use what it has not been shown.

corpusStats answers how large a (virtual) corpus is, textMetadata
retrieves everything KorAP knows about a text given its sigle, and
collocationScoreQuery scores collocation candidates that are already
known, as opposed to collocationAnalysis, which searches for them. The
latter also demonstrates passing a vector of collocates, which was added
in 1.3.0 but not shown anywhere either.

All examples and their output were run against DeReKo. The three
documentation prompting tasks added cover exactly these
functions, and GLM-5.3-Flash solves all three from the new sections
alone, including as.df = TRUE and the vector of collocates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Ia760750d10825bb2220c807ef0fc6e25e2cdd821
diff --git a/Readme.md b/Readme.md
index 34ad100..c492359 100644
--- a/Readme.md
+++ b/Readme.md
@@ -100,6 +100,55 @@
   fetchAll()
 ```
   
+### How big is my (virtual) corpus?
+
+`corpusStats` reports the size of the whole corpus or of a virtual corpus:
+
+```r
+library(RKorAPClient)
+kco <- KorAPConnection(verbose = TRUE)
+corpusStats(kco, vc = "pubDate since 2020")
+```
+
+```
+<KorAPCorpusStats>
+The virtual corpus described by "pubDate since 2020" contains 3,942,948,561 tokens
+in 253,752,552 sentences in 13,857,981 documents.
+```
+
+With `as.df = TRUE` you get a one row data frame with `tokens`, `sentences`, `paragraphs` and `documents` columns instead, which makes it easy to compare several virtual corpora.
+
+### Metadata of a text
+
+`textMetadata` retrieves all metadata KorAP holds for a text, given its sigle as found in the `textSigle` column of query results:
+
+```r
+KorAPConnection() |> textMetadata("WPD17/L79/98721")
+```
+
+|textSigle       |author                 |title    |pubDate    |textType     |
+|:---------------|:----------------------|:--------|:----------|:------------|
+|WPD17/L79/98721 |GeorgDerReisende, u.a. |Leverone |2017-07-01 |Enzyklopädie |
+
+The result has one column per metadata field the corpus provides – 26 in this example, so the table above shows only a selection.
+
+### Association scores for collocation candidates you already have
+
+While `collocationAnalysis` searches for collocates, `collocationScoreQuery` computes association scores for pairs you already have in mind. It accepts a vector of collocates and queries every combination of collocate and virtual corpus:
+
+```r
+KorAPConnection() |>
+  collocationScoreQuery("Grund", c("triftiger", "guter", "Berlin"))
+```
+
+|node  |collocate |        O|         E| logDice|   pmi|       ll|
+|:-----|:---------|--------:|---------:|-------:|-----:|--------:|
+|Grund |triftiger |  2390.06|      6.31|    0.64|  8.57| 26288.28|
+|Grund |guter     | 12902.28|   2713.05|    3.04|  2.25| 19938.66|
+|Grund |Berlin    |  7866.48|  26211.71|    2.03| -1.74| 17789.94|
+
+`O` is the observed and `E` the expected co-occurrence frequency. *Triftiger* is by far the most strongly attracted of the three (highest `pmi`), while *Berlin* co-occurs with *Grund* less often than chance would predict, which is what a negative `pmi` expresses.
+
 ### Identify *in … setzen* light verb constructions using `collocationAnalysis`
 
 ```r
diff --git a/tests/testthat/test-readme-against-llm.R b/tests/testthat/test-readme-against-llm.R
index 886ccf6..763ccde 100644
--- a/tests/testthat/test-readme-against-llm.R
+++ b/tests/testthat/test-readme-against-llm.R
@@ -369,4 +369,71 @@
       expect_true(execution_result, "Generated code should execute without runtime errors")
     }
   })
+
+  test_that(paste(model, "can solve corpus size task with README guidance"), {
+    skip_if_offline()
+    skip_if_no_api_key(model)
+    if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
+    skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
+
+    prompt <- create_readme_prompt(
+      "write R code that reports how many tokens the virtual corpus of newspaper texts published since 2020 contains.",
+      "Write R code to determine the size of a virtual corpus using RKorAPClient."
+    )
+
+    generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
+
+    expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
+    expect_true(grepl("corpusStats", generated_code), "Generated code should include corpusStats")
+    expect_true(grepl("vc", generated_code), "Generated code should restrict to a virtual corpus")
+    expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
+
+    cat("Generated corpus size code:\n", generated_code, "\n")
+  })
+
+  test_that(paste(model, "can solve text metadata task with README guidance"), {
+    skip_if_offline()
+    skip_if_no_api_key(model)
+    if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
+    skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
+
+    prompt <- create_readme_prompt(
+      "write R code that retrieves all metadata KorAP holds for the text with the sigle WPD17/L79/98721.",
+      "Write R code to retrieve the metadata of a text using RKorAPClient."
+    )
+
+    generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
+
+    expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
+    expect_true(grepl("textMetadata", generated_code), "Generated code should include textMetadata")
+    expect_true(grepl("WPD17/L79/98721", generated_code, fixed = TRUE), "Generated code should include the text sigle")
+    expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
+
+    cat("Generated text metadata code:\n", generated_code, "\n")
+  })
+
+  test_that(paste(model, "can solve association score task with README guidance"), {
+    skip_if_offline()
+    skip_if_no_api_key(model)
+    if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
+    skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
+
+    prompt <- create_readme_prompt(
+      paste(
+        "write R code that computes association scores for the word 'Grund' together with each of the",
+        "collocates 'triftiger' and 'guter', without searching for collocates first."
+      ),
+      "Write R code to compute association scores for known collocation candidates using RKorAPClient."
+    )
+
+    generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
+
+    expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
+    expect_true(grepl("collocationScoreQuery", generated_code), "Generated code should include collocationScoreQuery")
+    expect_true(grepl("triftiger", generated_code), "Generated code should include the collocate 'triftiger'")
+    expect_true(grepl("guter", generated_code), "Generated code should include the collocate 'guter'")
+    expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
+
+    cat("Generated association score code:\n", generated_code, "\n")
+  })
 }