diff --git a/resources/language_learning/vocabulary_estimation/article_controls.css b/resources/language_learning/vocabulary_estimation/article_controls.css
index 875ce951..511f2575 100644
--- a/resources/language_learning/vocabulary_estimation/article_controls.css
+++ b/resources/language_learning/vocabulary_estimation/article_controls.css
@@ -213,3 +213,168 @@
}
.article-controls-installation { display: contents; }
+
+.series-navigation,
+.article-active-contents,
+.article-contents-drawer {
+ --series-accent: #1464b5;
+ --series-muted: #4f5b66;
+ min-width: 0;
+ color: var(--bs-body-color, #212529);
+}
+.quarto-dark :is(.series-navigation, .article-active-contents, .article-contents-drawer) {
+ --series-accent: #73b7ff;
+ --series-muted: #b9c7d2;
+}
+.series-navigation {
+ margin: 0 0 1.25rem;
+ border: 1px solid var(--bs-border-color, #ced4da);
+ border-radius: .65rem;
+ background: var(--bs-body-bg, #fff);
+ overflow: hidden;
+}
+.series-navigation-links {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+ align-items: center;
+ gap: .75rem;
+ padding: .8rem 1rem;
+}
+.series-adjacent-link {
+ display: grid;
+ min-width: 0;
+ gap: .08rem;
+ border-radius: .35rem;
+ padding: .35rem .45rem;
+ text-decoration: none;
+}
+.series-adjacent-link[rel="next"] { text-align: right; }
+.series-adjacent-direction,
+.series-item-status,
+.article-contents-kind {
+ color: var(--series-muted);
+ font-size: .72rem;
+ font-weight: 750;
+ letter-spacing: .035em;
+ text-transform: uppercase;
+}
+.series-adjacent-title { overflow-wrap: anywhere; }
+.series-current-position {
+ border-radius: 999px;
+ padding: .3rem .65rem;
+ background: color-mix(in srgb, var(--bs-body-bg, #fff) 80%, var(--series-accent) 20%);
+ color: var(--bs-body-color, #212529);
+ font-size: .82rem;
+ font-weight: 800;
+ white-space: nowrap;
+}
+.series-roadmap { border-top: 1px solid var(--bs-border-color, #dee2e6); }
+.series-roadmap > summary {
+ padding: .65rem 1rem;
+ color: var(--series-accent);
+ font-weight: 750;
+ cursor: pointer;
+}
+.series-roadmap-body { padding: .15rem 1rem 1rem; }
+.series-roadmap-body h3 { margin: 1rem 0 .25rem; font-size: 1rem; }
+.series-opening-list,
+.series-future-list { margin-bottom: 0; }
+.series-opening-list li,
+.series-future-list li { min-width: 0; padding: .18rem 0; overflow-wrap: anywhere; }
+.series-item-status { display: inline-block; margin-left: .45rem; }
+.article-active-contents-shell { min-width: 0; }
+.article-active-contents {
+ --article-content-half-width: 23.5rem;
+ --article-contents-gap: 1rem;
+ --article-contents-width: 17rem;
+ --article-contents-viewport-gap: 1rem;
+ margin: 1rem 0 1.25rem;
+ border-left: 3px solid var(--series-accent);
+ padding: .65rem .8rem;
+ background: color-mix(in srgb, var(--bs-body-bg, #fff) 94%, var(--series-accent) 6%);
+}
+.article-active-contents h2,
+.article-contents-drawer h2 { margin: 0 0 .45rem; font-size: 1rem; }
+.article-contents-list { margin: 0; padding-left: 1.35rem; }
+.article-contents-list li { min-width: 0; padding: .15rem 0; overflow-wrap: anywhere; }
+.article-contents-list a { text-decoration: none; }
+.article-contents-list a.is-active,
+.article-contents-list a[aria-current="location"] {
+ color: var(--series-accent);
+ font-weight: 800;
+ text-decoration: underline;
+ text-decoration-thickness: .12em;
+ text-underline-offset: .16em;
+}
+.article-contents-kind { margin-left: .35rem; }
+.article-contents-open,
+.article-contents-close {
+ border: 1px solid var(--series-accent);
+ border-radius: .4rem;
+ padding: .5rem .75rem;
+ background: var(--bs-body-bg, #fff);
+ color: var(--bs-body-color, #212529);
+ font-weight: 750;
+ cursor: pointer;
+}
+.article-contents-open { display: none; margin: 0 0 1rem; }
+#article-contents-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 1045 !important;
+ background: rgb(0 0 0 / 48%);
+}
+#article-contents-drawer {
+ position: fixed;
+ inset: 0 0 0 auto;
+ z-index: 1060 !important;
+ width: min(88vw, 22rem);
+ padding: 1rem;
+ background: var(--bs-body-bg, #fff);
+ box-shadow: -.35rem 0 1.2rem rgb(0 0 0 / 24%);
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+.article-contents-drawer-header {
+ display: flex;
+ align-items: start;
+ justify-content: space-between;
+ gap: .75rem;
+ margin-bottom: .7rem;
+}
+body.article-contents-open { overflow: hidden; }
+:is(.series-adjacent-link, .series-roadmap > summary, .article-contents-list a,
+ .article-contents-open, .article-contents-close):focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--series-accent) 55%, transparent);
+ outline-offset: 3px;
+}
+@media (min-width: 1280px) {
+ .article-active-contents {
+ position: fixed;
+ left: calc(50% + var(--article-content-half-width) + var(--article-contents-gap));
+ top: 5rem;
+ z-index: 1;
+ width: min(var(--article-contents-width), calc(50vw - var(--article-content-half-width) - var(--article-contents-gap) - var(--article-contents-viewport-gap)));
+ max-height: calc(100dvh - 6rem);
+ margin: 0;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ }
+}
+@media (max-width: 991px) {
+ .article-active-contents { display: none; }
+ .article-contents-open { display: inline-flex; }
+ #article-contents-drawer { width: 100vw; max-width: none; }
+}
+@media (max-width: 575px) {
+ .series-navigation-links {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ }
+ .series-current-position { grid-column: 1 / -1; grid-row: 1; justify-self: center; }
+ .series-adjacent-placeholder { min-height: 1px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ #quarto-document-content,
+ .article-contents-drawer { transition: none !important; }
+ :root { scroll-behavior: auto !important; }
+}
diff --git a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css
index 2c18db1a..e0a55153 100644
--- a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css
+++ b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css
@@ -1 +1 @@
-:root{--ve-accent:#1464b5;--ve-accent-soft:#e5f1fb;--ve-warm:#9a4b00;--ve-warm-soft:#fff0df;--ve-purple:#694aa6;--ve-muted:#4f5b66}.quarto-dark{--ve-accent:#73b7ff;--ve-accent-soft:#173653;--ve-warm:#ffc27a;--ve-warm-soft:#4a2d12;--ve-purple:#c5a7ff;--ve-muted:#b9c7d2}#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}mjx-container[display=true]{max-width:100%;overflow-x:auto;overflow-y:hidden}.ve-callout{border:1px solid color-mix(in srgb,var(--ve-accent) 45%,var(--bs-border-color,#dee2e6));border-left:4px solid var(--ve-accent);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%);color:var(--bs-body-color,#212529);padding:1rem 1.15rem;margin:1.4rem 0;border-radius:.35rem}.ve-callout.provisional{border-color:color-mix(in srgb,var(--ve-warm) 60%,var(--bs-border-color,#dee2e6));border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}.ve-callout strong{display:block;margin-bottom:.3rem}.ve-callout p:last-child{margin-bottom:0}.ve-grid,.ve-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.75rem;margin:1.25rem 0}.ve-card,.ve-definition{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:.85rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);overflow-wrap:anywhere}.ve-card h3{font-size:1rem;margin-top:0}.ve-definition dt{font-weight:800;color:var(--ve-accent)}.ve-definition dd{margin:.25rem 0 0}.ve-table-wrap{max-width:100%;overflow-x:auto;margin:1.25rem 0}.ve-table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.ve-table th,.ve-table td{padding:.55rem .7rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-table th:first-child,.ve-table td:first-child{text-align:left}.ve-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}.ve-figure{margin:1.5rem 0;padding:1rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-figure svg{display:block;width:100%;height:auto}.ve-caption,.ve-note{font-size:.9rem;color:var(--ve-muted);margin:.75rem 0 0}.ve-simulator{margin:1.5rem 0}.ve-reading-guide{border-left:4px solid var(--ve-accent);padding:.2rem 0 .2rem 1rem;margin:1rem 0}.ve-reading-guide h3{font-size:1rem;margin:.1rem 0 .35rem}.ve-reading-guide ul{margin-bottom:.2rem}.ve-hierarchy{display:grid;grid-template-columns:minmax(8rem,.7fr) auto minmax(0,1.5fr);gap:.7rem;align-items:center}.ve-hierarchy-node{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.7rem;text-align:center;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.ve-hierarchy-node strong{display:block;color:var(--ve-accent)}.ve-hierarchy-arrow{font-size:1.5rem;color:var(--ve-accent);font-weight:800}.ve-form-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}.ve-sense-gap{grid-column:1/-1;border:2px dashed var(--ve-warm);border-radius:.45rem;padding:.65rem;text-align:center;color:var(--bs-body-color,#212529);background:color-mix(in srgb,var(--bs-body-bg,#fff) 92%,var(--ve-warm) 8%)}.ve-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:767px){.ve-hierarchy{grid-template-columns:minmax(0,1fr)}.ve-hierarchy-arrow{transform:rotate(90deg);text-align:center}.ve-form-list{grid-template-columns:minmax(0,1fr)}}@media(max-width:575px){.ve-table th,.ve-table td{padding:.45rem}.ve-figure{padding:.65rem}}
+:root{--ve-accent:#1464b5;--ve-accent-soft:#e5f1fb;--ve-warm:#9a4b00;--ve-warm-soft:#fff0df;--ve-purple:#694aa6;--ve-muted:#4f5b66}.quarto-dark{--ve-accent:#73b7ff;--ve-accent-soft:#173653;--ve-warm:#ffc27a;--ve-warm-soft:#4a2d12;--ve-purple:#c5a7ff;--ve-muted:#b9c7d2}#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}mjx-container[display=true]{max-width:100%;overflow-x:auto;overflow-y:hidden}.ve-attempt-brief{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));gap:.65rem;margin:0 0 1.4rem;padding:clamp(.8rem,2vw,1rem);border:1px solid color-mix(in srgb,var(--ve-accent) 45%,var(--bs-border-color,#dee2e6));border-left:5px solid var(--ve-accent);border-radius:.55rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 91%,var(--ve-accent) 9%)}.ve-attempt-brief>div{min-width:0;padding:.55rem .65rem;border-radius:.35rem;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.ve-attempt-brief span{display:block;font-size:.72rem;font-weight:800;letter-spacing:.055em;text-transform:uppercase;color:var(--ve-muted)}.ve-attempt-brief strong{display:block;margin-top:.15rem;font-variant-numeric:tabular-nums}.ve-callout{border:1px solid color-mix(in srgb,var(--ve-accent) 45%,var(--bs-border-color,#dee2e6));border-left:4px solid var(--ve-accent);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%);color:var(--bs-body-color,#212529);padding:1rem 1.15rem;margin:1.4rem 0;border-radius:.35rem}.ve-callout.provisional{border-color:color-mix(in srgb,var(--ve-warm) 60%,var(--bs-border-color,#dee2e6));border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}.ve-callout strong{display:block;margin-bottom:.3rem}.ve-callout p:last-child{margin-bottom:0}.ve-grid,.ve-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.75rem;margin:1.25rem 0}.ve-card,.ve-definition{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:.85rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);overflow-wrap:anywhere}.ve-card h3{font-size:1rem;margin-top:0}.ve-definition dt{font-weight:800;color:var(--ve-accent)}.ve-definition dd{margin:.25rem 0 0}.ve-table-wrap{max-width:100%;overflow-x:auto;margin:1.25rem 0}.ve-table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.ve-table th,.ve-table td{padding:.55rem .7rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-table th:first-child,.ve-table td:first-child{text-align:left}.ve-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}.ve-figure{margin:1.5rem 0;padding:1rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-figure svg{display:block;width:100%;height:auto}.ve-caption,.ve-note{font-size:.9rem;color:var(--ve-muted);margin:.75rem 0 0}.ve-simulator{margin:1.5rem 0}.ve-reading-guide{border-left:4px solid var(--ve-accent);padding:.2rem 0 .2rem 1rem;margin:1rem 0}.ve-reading-guide h3{font-size:1rem;margin:.1rem 0 .35rem}.ve-reading-guide ul{margin-bottom:.2rem}.ve-hierarchy{display:grid;grid-template-columns:minmax(8rem,.7fr) auto minmax(0,1.5fr);gap:.7rem;align-items:center}.ve-hierarchy-node{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.7rem;text-align:center;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.ve-hierarchy-node strong{display:block;color:var(--ve-accent)}.ve-hierarchy-arrow{font-size:1.5rem;color:var(--ve-accent);font-weight:800}.ve-form-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}.ve-sense-gap{grid-column:1/-1;border:2px dashed var(--ve-warm);border-radius:.45rem;padding:.65rem;text-align:center;color:var(--bs-body-color,#212529);background:color-mix(in srgb,var(--bs-body-bg,#fff) 92%,var(--ve-warm) 8%)}.ve-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:767px){.ve-hierarchy{grid-template-columns:minmax(0,1fr)}.ve-hierarchy-arrow{transform:rotate(90deg);text-align:center}.ve-form-list{grid-template-columns:minmax(0,1fr)}}@media(max-width:575px){.ve-table th,.ve-table td{padding:.45rem}.ve-figure{padding:.65rem}}
diff --git a/src/language_learning/vocabulary_estimation/article_controls.clj b/src/language_learning/vocabulary_estimation/article_controls.clj
index 574be100..48b46bb5 100644
--- a/src/language_learning/vocabulary_estimation/article_controls.clj
+++ b/src/language_learning/vocabulary_estimation/article_controls.clj
@@ -1,7 +1,64 @@
(ns language-learning.vocabulary-estimation.article-controls
(:require [clojure.java.io :as io]
+ [clojure.string :as str]
[scicloj.kindly.v4.kind :as kind]))
+(def series-manifest
+ [{:id :workflow
+ :number 1
+ :title "Managing brilliant but uneven minds"
+ :url "managing_brilliant_but_uneven_minds.html"
+ :status :published}
+ {:id :purpose
+ :number 2
+ :title "Why estimate vocabulary?"
+ :url "why_estimate_vocabulary.html"
+ :status :published}
+ {:id :bayes
+ :number 3
+ :title "Bayes' theorem: from uncertainty to decision"
+ :url "bayes_theorem_simulations.html"
+ :status :published}
+ {:id :proposal-1
+ :number 4
+ :title "Proposal 1: estimating known pairs"
+ :url "beta_binomial_first_pass.html"
+ :status :research-target}
+ {:id :proposal-2
+ :number 5
+ :title "Proposal 2: does pair frequency predict responses?"
+ :url "pair_frequency_logistic_v2_article.html"
+ :status :not-promoted}])
+
+(def series-roadmap
+ [{:id :pool-construction
+ :title "Build and version the lemma–surface-form pool"
+ :status :deferred}
+ {:id :lemma-inference
+ :title "Infer lemma knowledge from correlated form evidence"
+ :status :deferred}
+ {:id :response-model
+ :title "Model correct, wrong, and don't-know responses separately"
+ :status :deferred}
+ {:id :calibration-and-adaptation
+ :title "Calibrate items before shortening tests adaptively"
+ :status :deferred}
+ {:id :contexts-and-senses
+ :title "Decide when contexts and senses become identifiable"
+ :status :deferred}])
+
+(defn series-context
+ "Return the current article and its immediate neighbours in canonical order."
+ [article-id]
+ (let [index (first (keep-indexed #(when (= article-id (:id %2)) %1)
+ series-manifest))]
+ (when (nil? index)
+ (throw (ex-info "Unknown vocabulary-series article id."
+ {:article-id article-id})))
+ {:previous (get series-manifest (dec index))
+ :current (get series-manifest index)
+ :next (get series-manifest (inc index))}))
+
(defn- resource-text [filename]
(slurp
(io/resource
@@ -42,12 +99,161 @@
{:aria-live "polite"}
"Equations are shown."]]])
+(def status-labels
+ {:published "published"
+ :research-target "current research target"
+ :not-promoted "published; not promoted"
+ :deferred "deferred"})
+
+(defn- json-escape [text]
+ (-> (str text)
+ (str/replace "\\" "\\\\")
+ (str/replace "\"" "\\\"")
+ (str/replace "\n" "\\n")
+ (str/replace "\r" "\\r")
+ (str/replace "\t" "\\t")
+ (str/replace "<" "\\u003c")))
+
+(declare json-value)
+
+(defn- json-object [value]
+ (str "{"
+ (str/join
+ ","
+ (map (fn [[key item]]
+ (str (json-value (name key)) ":" (json-value item)))
+ value))
+ "}"))
+
+(defn- json-value [value]
+ (cond
+ (map? value) (json-object value)
+ (sequential? value) (str "[" (str/join "," (map json-value value)) "]")
+ (keyword? value) (json-value (name value))
+ (string? value) (str "\"" (json-escape value) "\"")
+ (number? value) (str value)
+ (true? value) "true"
+ (false? value) "false"
+ (nil? value) "null"
+ :else (json-value (str value))))
+
+(defn- article-link [{:keys [url title number]} relationship]
+ (when url
+ [:a.series-adjacent-link
+ (cond-> {:href url}
+ relationship (assoc :rel relationship))
+ [:span.series-adjacent-direction
+ (case relationship
+ "prev" "Previous"
+ "next" "Next"
+ "Article")]
+ [:span.series-adjacent-title (str number ". " title)]]))
+
+(defn- roadmap [article-id]
+ [:details.series-roadmap
+ [:summary "Open the five-article series and future roadmap"]
+ [:div.series-roadmap-body
+ [:ol.series-opening-list
+ (for [{:keys [id number title url status]} series-manifest]
+ [:li
+ [:a (cond-> {:href url}
+ (= id article-id) (assoc :aria-current "page"))
+ (str number ". " title)]
+ [:span.series-item-status (get status-labels status (name status))]])]
+ [:h3 "After the opening five"]
+ [:p "These questions remain deliberately unnumbered until their research decisions are ready."]
+ [:ul.series-future-list
+ (for [{:keys [id title status]} series-roadmap]
+ [:li
+ title
+ [:span.series-item-status (get status-labels status (name status))]])]]])
+
+(defn- series-navigation [article-id]
+ (let [{:keys [previous current next]} (series-context article-id)]
+ [:nav.series-navigation {:aria-label "Vocabulary-estimation article series"}
+ [:div.series-navigation-links
+ (or (article-link previous "prev")
+ [:span.series-adjacent-placeholder {:aria-hidden "true"}])
+ [:span.series-current-position {:aria-current "page"}
+ (str "Article " (:number current) " of " (count series-manifest))]
+ (or (article-link next "next")
+ [:span.series-adjacent-placeholder {:aria-hidden "true"}])]
+ (roadmap article-id)]))
+
+(defn- contents-list [sections]
+ [:ol.article-contents-list
+ (for [{:keys [id label technical]} sections]
+ [:li {:class (when technical "article-contents-technical")}
+ [:a {:href (str "#" id)
+ :data-section-id id}
+ label]
+ (when technical
+ [:span.article-contents-kind "technical"])])])
+
+(defn- active-contents [sections]
+ [:div.article-active-contents-shell
+ [:button#article-contents-open.article-contents-open
+ {:type "button"
+ :aria-expanded "false"
+ :aria-controls "article-contents-drawer"}
+ "Contents"]
+ [:nav.article-active-contents
+ {:aria-labelledby "article-contents-heading"}
+ [:h2#article-contents-heading "In this article"]
+ (contents-list sections)]
+ [:div#article-contents-backdrop.article-contents-backdrop {:hidden true}]
+ [:aside#article-contents-drawer.article-contents-drawer
+ {:aria-hidden "true"
+ :aria-modal "true"
+ :aria-labelledby "article-contents-drawer-heading"
+ :role "dialog"
+ :hidden true
+ :tabindex "-1"}
+ [:div.article-contents-drawer-header
+ [:h2#article-contents-drawer-heading "In this article"]
+ [:button#article-contents-close.article-contents-close
+ {:type "button" :aria-label "Close article contents"}
+ "Close"]]
+ [:nav {:aria-label "Article contents drawer"}
+ (contents-list sections)]]])
+
+(defn- configured-install
+ [{:keys [article-id sections technical-sections]}]
+ (let [all-sections
+ (vec (concat (map #(assoc % :technical false) sections)
+ (map #(assoc % :technical true) technical-sections)))
+ section-ids (mapv :id all-sections)]
+ (series-context article-id)
+ (when (or (empty? all-sections)
+ (some str/blank? section-ids)
+ (not= (count section-ids) (count (distinct section-ids))))
+ (throw (ex-info "Article sections require unique, non-blank ids."
+ {:sections all-sections})))
+ (kind/hiccup
+ [:div.article-controls-installation
+ [:style (resource-text "article_controls.css")]
+ (series-navigation article-id)
+ (active-contents all-sections)
+ (toolbar)
+ [:script
+ {:id "article-controls-config"
+ :type "application/json"}
+ (json-value
+ {:articleId (name article-id)
+ :sections (mapv #(select-keys % [:id :label :technical])
+ all-sections)})]
+ [:script {:type "application/x-scittle"
+ :src "article_controls.cljs"}]])))
+
(defn install
- "Install the controls stylesheet, explicit toolbar, and Scittle runtime."
- []
- (kind/hiccup
- [:div.article-controls-installation
- [:style (resource-text "article_controls.css")]
- (toolbar)
- [:script {:type "application/x-scittle"
- :src "article_controls.cljs"}]]))
+ "Install article controls. The zero-argument form preserves the migration
+ shell; the configuration form adds the canonical series and contents UI."
+ ([]
+ (kind/hiccup
+ [:div.article-controls-installation
+ [:style (resource-text "article_controls.css")]
+ (toolbar)
+ [:script {:type "application/x-scittle"
+ :src "article_controls.cljs"}]]))
+ ([configuration]
+ (configured-install configuration)))
diff --git a/src/language_learning/vocabulary_estimation/article_controls.cljs b/src/language_learning/vocabulary_estimation/article_controls.cljs
index 47a7fcea..67f4e41b 100644
--- a/src/language_learning/vocabulary_estimation/article_controls.cljs
+++ b/src/language_learning/vocabulary_estimation/article_controls.cljs
@@ -31,6 +31,62 @@
:code code
:equations equations}))
+(defn drawer-transition
+ "Return the drawer's next public state and the focus destination."
+ [{:keys [open?]} event]
+ (case event
+ :open {:open? true :focus :drawer-close}
+ :close {:open? false :focus :trigger}
+ :escape (if open?
+ {:open? false :focus :trigger}
+ {:open? false :focus nil})
+ {:open? (boolean open?) :focus nil}))
+
+(defn active-section-id
+ "Choose an active contents id from a valid hash or section top positions."
+ [sections hash-text activation-offset]
+ (let [hash-id (when (and (string? hash-text)
+ (str/starts-with? hash-text "#"))
+ (subs hash-text 1))
+ ids (set (map :id sections))]
+ (or (when (contains? ids hash-id) hash-id)
+ (some->> sections
+ (filter #(<= (:top %) activation-offset))
+ last
+ :id)
+ (:id (first sections)))))
+
+(defn anchor-navigation
+ "Describe direct-anchor navigation without requiring a DOM."
+ [section-id reduced-motion?]
+ {:hash (str "#" section-id)
+ :history :native
+ :scroll (if reduced-motion? :instant :smooth)
+ :focus section-id})
+
+(defn section-id-from-href
+ "Read a section id from either a fragment-only or absolute same-page href."
+ [href]
+ (when-let [hash-index (and (string? href) (str/last-index-of href "#"))]
+ (let [section-id (subs href (inc hash-index))]
+ (when-not (str/blank? section-id)
+ section-id))))
+
+(def editable-tags #{"input" "textarea" "select"})
+
+(defn editable-context?
+ [{:keys [tag-name content-editable? role]}]
+ (or (contains? editable-tags (str/lower-case (or tag-name "")))
+ (true? content-editable?)
+ (= "textbox" (str/lower-case (or role "")))))
+
+(defn global-key-handled?
+ "The series controller owns Escape only for an open drawer outside editors."
+ [key context drawer-open?]
+ (and drawer-open?
+ (= "Escape" key)
+ (not (editable-context? context))))
+
(defn dom-available? []
(exists? js/document))
@@ -353,7 +409,171 @@
" equation code disclosures.")))
equation-details)))
+(defn- element-editable-context [element]
+ (let [editable-root
+ (when (and element (.-closest element))
+ (.closest element
+ "input,textarea,select,[contenteditable],[role=textbox]"))]
+ {:tag-name (some-> editable-root .-tagName)
+ :content-editable? (boolean (some-> editable-root .-isContentEditable))
+ :role (some-> editable-root (.getAttribute "role"))}))
+
+(defn- initialise-series-controls! []
+ (when-let [config-node
+ (.getElementById js/document "article-controls-config")]
+ (when-not (= "true" (data-value config-node "initialized"))
+ (let [config
+ (js->clj
+ (js/JSON.parse (or (.-textContent config-node) "{}"))
+ :keywordize-keys true)
+ sections (:sections config)
+ open-button (.getElementById js/document "article-contents-open")
+ close-button (.getElementById js/document "article-contents-close")
+ drawer (.getElementById js/document "article-contents-drawer")
+ backdrop (.getElementById js/document "article-contents-backdrop")
+ contents-links
+ (q-all js/document
+ ".article-contents-list a[data-section-id]")
+ drawer-state (atom {:open? false :focus nil})
+ reduced-motion-query
+ (.matchMedia js/window "(prefers-reduced-motion: reduce)")
+ activation-offset 112
+ frame (atom nil)]
+ (aset (.-dataset config-node) "initialized" "true")
+ (doseq [link contents-links
+ :let [section-id (data-value link "sectionId")]
+ :when section-id]
+ (.setAttribute link "href" (str "#" section-id))
+ (.removeAttribute link "data-original-href"))
+ (letfn [(section-elements []
+ (mapv
+ (fn [{:keys [id]}]
+ {:id id
+ :element (.getElementById js/document id)})
+ sections))
+ (set-active! [section-id]
+ (doseq [link contents-links]
+ (let [active? (= section-id
+ (data-value link "sectionId"))]
+ (.toggle (.-classList link)
+ "is-active" active?)
+ (if active?
+ (.setAttribute link "aria-current" "location")
+ (.removeAttribute link "aria-current")))))
+ (focus! [destination]
+ (case destination
+ :drawer-close (some-> close-button .focus)
+ :trigger (some-> open-button .focus)
+ nil))
+ (render-drawer! [state]
+ (let [open? (:open? state)]
+ (reset! drawer-state state)
+ (when open-button
+ (.setAttribute open-button "aria-expanded" (str open?)))
+ (when drawer
+ (set! (.-hidden drawer) (not open?))
+ (.setAttribute drawer "aria-hidden" (str (not open?))))
+ (when backdrop
+ (set! (.-hidden backdrop) (not open?)))
+ (.toggle (.-classList (.-body js/document))
+ "article-contents-open" open?)
+ (focus! (:focus state))))
+ (transition-drawer! [event]
+ (render-drawer!
+ (drawer-transition @drawer-state event)))
+ (focus-section! [element]
+ (when element
+ (when-not (.hasAttribute element "tabindex")
+ (.setAttribute element "tabindex" "-1")
+ (aset (.-dataset element)
+ "articleControlsTabindex" "temporary"))
+ (.focus element #js {:preventScroll true})))
+ (navigate! [section-id]
+ (when-let [target (.getElementById js/document section-id)]
+ (let [{:keys [scroll]}
+ (anchor-navigation
+ section-id (.-matches reduced-motion-query))]
+ (.scrollIntoView
+ target
+ #js {:behavior (if (= :smooth scroll)
+ "smooth" "auto")
+ :block "start"})
+ (set-active! section-id)
+ (render-drawer! {:open? false :focus nil})
+ (focus-section! target))))
+ (sync-active-from-scroll! []
+ (let [metrics
+ (->> (section-elements)
+ (keep (fn [{:keys [id element]}]
+ (when element
+ {:id id
+ :top (.-top
+ (.getBoundingClientRect
+ element))})))
+ vec)]
+ (set-active!
+ (active-section-id metrics "" activation-offset))))
+ (schedule-active-sync! []
+ (when (nil? @frame)
+ (reset!
+ frame
+ (.requestAnimationFrame
+ js/window
+ (fn []
+ (reset! frame nil)
+ (sync-active-from-scroll!))))))
+ (sync-from-location! []
+ (let [hash-text (.-hash (.-location js/window))
+ section-id (when (str/starts-with? hash-text "#")
+ (subs hash-text 1))
+ valid-ids (set (map :id sections))]
+ (when (contains? valid-ids section-id)
+ (navigate! section-id))))]
+ (render-drawer! @drawer-state)
+ (when open-button
+ (.addEventListener
+ open-button "click" #(transition-drawer! :open)))
+ (when close-button
+ (.addEventListener
+ close-button "click" #(transition-drawer! :close)))
+ (when backdrop
+ (.addEventListener
+ backdrop "click" #(transition-drawer! :close)))
+ (.addEventListener
+ js/document "click"
+ (fn [event]
+ (let [target (.-target event)
+ link (when (and target (.-closest target))
+ (.closest
+ target
+ ".article-contents-list a[data-section-id]"))]
+ (when link
+ (let [section-id
+ (or (data-value link "sectionId")
+ (section-id-from-href
+ (.getAttribute link "href")))]
+ (js/setTimeout #(navigate! section-id) 0))))))
+ (.addEventListener
+ js/document "keydown"
+ (fn [event]
+ (let [target (or (.-target event) (.-activeElement js/document))]
+ (when (global-key-handled?
+ (.-key event)
+ (element-editable-context target)
+ (:open? @drawer-state))
+ (.preventDefault event)
+ (transition-drawer! :escape))))
+ #js {:capture true})
+ (.addEventListener js/window "scroll" schedule-active-sync!
+ #js {:passive true})
+ (.addEventListener js/window "resize" schedule-active-sync!)
+ (.addEventListener js/window "hashchange" sync-from-location!)
+ (.addEventListener js/window "popstate" sync-from-location!)
+ (sync-active-from-scroll!)
+ (sync-from-location!))))))
+
(defn- initialise! []
+ (initialise-series-controls!)
(when-let [article-main
(or (.getElementById js/document "quarto-document-content")
(q js/document "main"))]
diff --git a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj
index 8191d3fd..314d2674 100644
--- a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj
+++ b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj
@@ -1,13 +1,14 @@
^{:kindly/hide-code true
:kindly/options {:html/deps [:scittle :reagent]}
:clay {:hide-info-line true
- :title "Bayes' Theorem from Uncertainty to Decision"
+ :title "Bayes' theorem: from uncertainty to decision"
:quarto {:author :jamiep
- :description "A visual, interactive introduction to Bayesian updating, posterior sampling, decisions, and Gaussian grid approximation."
+ :subtitle "Four executable simulations, from globe tosses to a finite vocabulary-pair prediction"
+ :description "Four visual, interactive simulations introduce Bayesian updating, posterior sampling, decisions, Gaussian grid approximation, and finite-pool vocabulary prediction."
:type :post
- :date "2026-07-13"
+ :date "2026-07-17"
:image "bayes_theorem_simulations_preview.png"
- :image-alt "Three Gaussian parameter-grid heatmaps show the posterior before and after a seeded height observation, with playback controls."
+ :image-alt "A finite-pool posterior-predictive chart follows binary responses for lemma–surface-form pairs."
:category :concepts
:tags [:bayesian-statistics :clojure :clojurescript :scittle :simulation]
:keywords [:bayes-theorem :grid-approximation :posterior-sampling :normal-distribution :data-visualisation]}}}
@@ -70,6 +71,7 @@
".bp-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.7rem;margin:1rem 0}"
".bp-definition{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.75rem;background:var(--bs-body-bg,#fff)}"
".bp-definition dt{font-weight:800;color:var(--bp-accent)}.bp-definition dd{margin:.25rem 0 0}"
+ ".bp-pair-prompt{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-left:4px solid var(--bp-accent);border-radius:.45rem;padding:.85rem 1rem;margin:1rem 0;background:color-mix(in srgb,var(--bs-body-bg,#fff) 92%,var(--bp-accent) 8%);overflow-wrap:anywhere}.bp-pair-prompt p:last-child{margin-bottom:0}"
".bp-engineering-caption{text-align:center;color:var(--bp-muted);font-size:.88rem}"
".series-toc{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.6rem;padding:clamp(.85rem,3vw,1.2rem);margin:0 0 1.4rem;background:var(--bs-body-bg,#fff)}"
".series-toc h2{font-size:1.2rem;margin:0 0 .55rem}.series-toc p{margin:0 0 .7rem}.series-toc ol{margin:0;padding-left:1.45rem}.series-toc li{padding:.18rem .45rem}"
@@ -78,37 +80,27 @@
"@media(max-width:767px){.bp-process-strip{grid-template-columns:minmax(0,1fr)}.bp-process-symbol{min-height:1.2rem}.bp-shell{padding:.75rem}.bp-controls{align-items:stretch}.bp-button{flex:1}.bp-chart{padding:.5rem}}")])
^:kindly/hide-code
-(kind/hiccup
- [:nav.series-toc {:aria-labelledby "series-contents-heading"}
- [:h2#series-contents-heading "Theory to vocabulary-estimation series"]
- [:p "Article 0 explains the workflow; this article supplies probability tools used by the later vocabulary-estimation articles."]
- [:ol {:start 0}
- [:li
- [:a {:href "managing_brilliant_but_uneven_minds.html"}
- "Managing brilliant but uneven minds: my theory-to-algorithm workflow"]
- [:span.series-status "published"]]
- [:li.series-current
- [:a {:href "bayes_theorem_simulations.html" :aria-current "page"}
- "Bayes' theorem from uncertainty to decision"]
- [:span.series-status "you are here"]]
- [:li [:a {:href "beta_binomial_first_pass.html"} "Estimating vocabulary size with a simple Bayesian model"] [:span.series-status "published"]]
- [:li [:a {:href "pair_frequency_logistic_v2_article.html"} "Does pair frequency predict learner responses?"] [:span.series-status "published"]]
- [:li "From Self-Reported CEFR to a Versioned Lemma–Form-Pair Pool" [:span.series-status "planned"]]
- [:li "From Correlated Form Pairs to Latent Lemma Knowledge" [:span.series-status "planned"]]
- [:li "Modelling Correct, Wrong, and Don't-Know Separately" [:span.series-status "planned"]]
- [:li "Calibrating Items Before IRT and Adaptive Selection" [:span.series-status "planned"]]
- [:li "When Contexts and Senses Become Identifiable" [:span.series-status "planned"]]]])
+(controls/install
+ {:article-id :bayes
+ :sections [{:id "five-ideas" :label "Five ideas under every update"}
+ {:id "globe-update" :label "Update from globe tosses"}
+ {:id "posterior-decision" :label "Sample and decide"}
+ {:id "gaussian-grid" :label "Scale up to two parameters"}
+ {:id "vocabulary-bridge" :label "Predict a finite pair total"}]
+ :technical-sections [{:id "reproducibility" :label "Reproduce the lesson"}
+ {:id "sources" :label "Sources"}]})
;; A probability model is a disciplined way to reason when one answer is
;; unknown. It does not remove uncertainty. It records candidate answers,
;; states how observations would arise under each candidate, and updates their
;; relative plausibility when data arrive.
;;
-;; This tutorial rebuilds three simulations I first made with Daniel Slutsky's
+;; This tutorial retains three simulations I first made with Daniel Slutsky's
;; [JointProb group](https://scicloj.github.io/docs/community/groups/jointprob/).
;; The examples come from sections 2.2, 3.2, and 4.3 of Richard McElreath's
-;; *Statistical Rethinking*. No statistics or programming background is
-;; assumed. Each chapter asks one practical question:
+;; *Statistical Rethinking*. A fourth simulation carries the same ideas into
+;; the vocabulary-estimation series. No statistics or programming background
+;; is assumed. Each chapter asks one practical question:
^:kindly/hide-code
(kind/hiccup
@@ -116,12 +108,10 @@
[:li [:strong "Update"] [:br] "How should observations change uncertainty?"]
[:li [:strong "Sample and decide"] [:br] "How can a distribution guide an action?"]
[:li [:strong "Scale up"] [:br] "How does the same update work with two unknown parameters?"]
+ [:li [:strong "Bridge"] [:br] "How can responses predict a finite known-pair total with visible uncertainty?"]
[:li [:strong "Reproduce"] [:br] "How do code, seeds, tests, and rendering make the lesson checkable?"]])
-^:kindly/hide-code
-(controls/install)
-
-;; ## The five ideas under every update
+;; ## The five ideas under every update {#five-ideas}
;;
;; Suppose I do not know what proportion of a globe is covered by water.
;; **Uncertainty** means more than one answer remains credible. A **probability**
@@ -197,7 +187,7 @@
[:div.bp-process-symbol {:aria-hidden "true"} "→"]
[:div.bp-process-step [:strong "Posterior"] [:small "normalised update"]]])
-;; ## 1. Update: learning from globe tosses
+;; ## 1. Update: learning from globe tosses {#globe-update}
;;
;; Imagine tossing and catching a globe. The point under a finger is recorded
;; as water or land. If a candidate says water is common, water observations
@@ -253,6 +243,98 @@
1.0
(range 1 (inc k)))))
+^:kindly/hide-code
+(defn beta-posterior-parameters
+ "Update Beta prior parameters from binary pair-recognition counts."
+ [{:keys [alpha beta]} {:keys [recognized not-recognized]}]
+ {:pre [(pos? alpha)
+ (pos? beta)
+ (<= 0 recognized)
+ (<= 0 not-recognized)]}
+ {:alpha (+ (double alpha) recognized)
+ :beta (+ (double beta) not-recognized)})
+
+^:kindly/hide-code
+(defn finite-pool-predictive-mean
+ "Posterior predictive mean for total known pairs, including observed pairs."
+ [pool-size recognized not-recognized alpha beta]
+ (let [observed (+ recognized not-recognized)
+ remaining (- pool-size observed)]
+ {:pre [(<= 0 observed pool-size)
+ (pos? alpha)
+ (pos? beta)]}
+ (+ recognized (* remaining (/ alpha (+ alpha beta))))))
+
+^{:kindly/hide-code true
+ :kindly/kind :kind/hidden}
+(defn- seeded-uniform! [state]
+ (let [next-state (mod (* 48271 @state) 2147483647)]
+ (reset! state next-state)
+ (/ (dec next-state) 2147483646.0)))
+
+^{:kindly/hide-code true
+ :kindly/kind :kind/hidden}
+(defn- integer-gamma-sample!
+ "Gamma(shape, 1) draw for a positive integer shape."
+ [state shape]
+ {:pre [(pos? shape) (== shape (Math/rint shape))]}
+ (reduce + (repeatedly (long shape)
+ #(- (Math/log (max 1.0e-15
+ (seeded-uniform! state)))))))
+
+^{:kindly/hide-code true
+ :kindly/kind :kind/hidden}
+(defn- beta-sample! [state alpha beta]
+ (let [x (integer-gamma-sample! state alpha)
+ y (integer-gamma-sample! state beta)]
+ (/ x (+ x y))))
+
+^{:kindly/hide-code true
+ :kindly/kind :kind/hidden}
+(defn- binomial-sample! [state trials probability]
+ (reduce (fn [successes _]
+ (if (< (seeded-uniform! state) probability)
+ (inc successes)
+ successes))
+ 0
+ (range trials)))
+
+^:kindly/hide-code
+(defn seeded-finite-pool-predictive
+ "Draw a replayable posterior predictive distribution for the total known
+ pairs in a finite pool. The Beta parameters must be positive integers; this
+ article uses Beta(1,1), so binary counts preserve that condition."
+ [{:keys [pool-size recognized not-recognized prior-alpha prior-beta
+ draw-count seed]}]
+ (let [observed (+ recognized not-recognized)
+ remaining (- pool-size observed)
+ {:keys [alpha beta] :as posterior}
+ (beta-posterior-parameters
+ {:alpha prior-alpha :beta prior-beta}
+ {:recognized recognized :not-recognized not-recognized})
+ state (atom (long seed))
+ totals (vec
+ (repeatedly
+ draw-count
+ (fn []
+ (let [knowing-rate (beta-sample! state alpha beta)]
+ (+ recognized
+ (binomial-sample! state remaining knowing-rate))))))
+ ordered (vec (sort totals))
+ quantile (fn [probability]
+ (nth ordered
+ (long (Math/floor (* probability
+ (dec draw-count))))))]
+ {:pre [(<= 0 observed pool-size)
+ (pos? draw-count)
+ (pos? seed)]}
+ {:posterior posterior
+ :mean (finite-pool-predictive-mean
+ pool-size recognized not-recognized alpha beta)
+ :lower (quantile 0.025)
+ :upper (quantile 0.975)
+ :frequencies (frequencies totals)}))
+
;; When only the counts matter, there are
;;
;; $$\binom{W+L}{W}=\frac{(W+L)!}{W!L!}$$
@@ -440,7 +522,7 @@
[:strong "Chapter 1 recap"]
[:p "Build: list candidate values and specify a prior and likelihood. Check: normalise and inspect the update after known observations. Decide: carry the posterior forward as the prior for the next observation."]])
-;; ## 2. Sample: turning uncertainty into a decision
+;; ## 2. Sample: turning uncertainty into a decision {#posterior-decision}
;;
;; A posterior is a distribution, not automatically a single estimate. To act,
;; we must say what action is available and what mistakes cost. A **decision**
@@ -566,7 +648,7 @@
[:strong "Chapter 2 recap"]
[:p "Build: define the action and its loss. Check: compare the direct grid minimum with a seeded Monte Carlo approximation. Decide: report the estimate appropriate to the declared loss, not a context-free ‘best’ number."]])
-;; ## 3. Scale up: a Gaussian model with two parameters
+;; ## 3. Scale up: a Gaussian model with two parameters {#gaussian-grid}
;;
;; The globe model had one parameter. Adult height introduces two. A
;; **Gaussian distribution**—the familiar symmetric bell shape—is described by
@@ -677,7 +759,129 @@
[:strong "Chapter 3 recap"]
[:p "Build: cross candidate means and standard deviations into a two-parameter grid. Check: inspect each likelihood and the before/after surfaces. Decide: retain the complete posterior surface, not only its highest cell."]])
-;; ## 4. Make the lesson reproducible
+;; ## 4. Bridge to a finite vocabulary-pair total {#vocabulary-bridge}
+;;
+;; Now replace water and land with two responses about a
+;; **lemma–surface-form pair**: recognized or not recognized. The interaction
+;; uses a declared pool of exactly 100 pairs and a fixed 12-pair teaching
+;; fixture. Before any responses, its **Beta(1,1) prior** gives equal density to
+;; every possible pair-knowing rate $p$ from 0 to 1. It is deliberately broad,
+;; not a claim about real learners.
+;;
+;; A recognized response contributes likelihood $p$; a not-recognized response
+;; contributes likelihood $1-p$. If $r$ pairs were recognized and $u$ were not,
+;; the posterior is
+;;
+;; $$p \mid r,u \sim \operatorname{Beta}(1+r,1+u).$$
+
+^:kindly/hide-code
+(math/explanation
+ "math-pair-beta-posterior"
+ "Beta posterior after binary pair-recognition responses"
+ [["p" "The unknown proportion of pairs that would be recognized under this teaching model."]
+ ["r" "The number of observed pairs marked recognized."]
+ ["u" "The number of observed pairs marked not recognized."]
+ ["Beta(1+r,1+u)" "The updated distribution after adding recognized and not-recognized counts to the Beta(1,1) prior."]]
+ "This compact update treats the binary responses as exchangeable and error-free; later articles must examine whether those assumptions are defensible.")
+
+^:kindly/hide-code
+(math/equation-code-detail
+ "code-pair-beta-posterior"
+ "Updating the declared Beta(1,1) prior"
+ {:kind :source
+ :label "bayes_theorem_simulations.clj — beta-posterior-parameters"
+ :href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj"
+ :symbols [["alpha" "The first Beta parameter: prior 1 plus recognized count r."]
+ ["beta" "The second Beta parameter: prior 1 plus not-recognized count u."]
+ ["recognized" "The implementation name for r."]
+ ["not-recognized" "The implementation name for u."]]}
+ [:div
+ [:pre [:code "(defn beta-posterior-parameters\n [{:keys [alpha beta]} {:keys [recognized not-recognized]}]\n {:alpha (+ alpha recognized)\n :beta (+ beta not-recognized)})"]]
+ [:p "The prior parameters and observed counts stay explicit rather than being hidden in browser state."]])
+
+;; A posterior describes uncertainty about $p$. A **posterior predictive
+;; distribution** asks a different question: after this update, which totals
+;; might the unasked pairs produce? The observed recognized pairs are already
+;; known inside the model, so the prediction adds them to a draw for the
+;; remaining pairs:
+;;
+;; $$K=r+K_{\text{unasked}},\qquad
+;; K_{\text{unasked}}\mid p\sim
+;; \operatorname{Binomial}(100-r-u,p).$$
+
+^:kindly/hide-code
+(math/explanation
+ "math-finite-pool-predictive"
+ "Posterior prediction of the finite known-pair total"
+ [["K" "The predicted total known pairs in the declared 100-pair pool."]
+ ["K_unasked" "The unknown number of known pairs among those not yet asked."]
+ ["r" "Observed recognized pairs, included directly in the total."]
+ ["100 − r − u" "The number of unasked pairs remaining in the finite pool."]
+ ["Binomial(n,p)" "A distribution for the count of successes among n binary trials at rate p."]]
+ "Each predictive draw first samples p from the posterior, then samples the unasked known-pair count conditional on that p.")
+
+^:kindly/hide-code
+(math/equation-code-detail
+ "code-finite-pool-predictive"
+ "Drawing a finite-pool known-pair total"
+ {:kind :source
+ :label "bayes_theorem_simulations.clj — seeded-finite-pool-predictive"
+ :href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/bayes_theorem_simulations.clj"
+ :symbols [["pool-size" "The finite total 100 represented by K."]
+ ["recognized" "Observed known-pair count r, added to every prediction."]
+ ["not-recognized" "Observed not-known count u, excluded from the unasked count."]
+ ["knowing-rate" "One draw of p from the Beta posterior."]
+ ["remaining" "The implementation name for 100 − r − u."]]}
+ [:div
+ [:pre [:code "(let [knowing-rate (beta-sample! state alpha beta)]\n (+ recognized\n (binomial-sample! state remaining knowing-rate)))"]]
+ [:p "Repeating this seeded draw 4,000 times produces the distribution shown by the interactive chart."]])
+
+^:kindly/hide-code
+(def bridge-example-predictive
+ (seeded-finite-pool-predictive
+ {:pool-size 100
+ :recognized 7
+ :not-recognized 3
+ :prior-alpha 1
+ :prior-beta 1
+ :draw-count 4000
+ :seed 620260717}))
+
+^:kindly/hide-code
+(kind/hiccup
+ [:details.bp-details
+ [:summary "Worked numerical checkpoint: 7 recognized, 3 not recognized"]
+ [:div
+ [:p "The prior Beta(1,1) becomes Beta(8,4). Ninety pairs remain unasked. The posterior-predictive mean total is 7 + 90 × 8/12 = 67.0 known pairs."]
+ [:p "With seed 620260717, 4,000 predictive draws give a central 95% interval from 42 to 89. Resetting and replaying the same response counts reproduces that interval exactly."]]])
+
+;; The predictive mean is one possible **decision**: a single count to report
+;; when squared error is the declared loss. The 95% interval keeps
+;; **uncertainty** visible rather than pretending the decision is exact. Change
+;; the binary responses below and watch the prior become a posterior, then a
+;; posterior prediction for the complete finite pool.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.bp-simulator
+ [:div#vocabulary-pair-simulator
+ [:p "Loading the finite-pool vocabulary-pair simulator…"]]
+ [:noscript "This simulator needs JavaScript. The equations and worked checkpoint above remain available without it."]])
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.bp-callout
+ [:strong "Boundary: demonstration, not validation"]
+ [:p "This is a teaching and model-behavior demonstration, not learner validation. Its pair prompts are not representative learners or calibrated LexiBench items. It predicts only the intermediate total of known lemma–surface-form pairs; it does not infer known lemmas."]
+ [:p "The bridge prepares the reader for the next article but does not implement or change Proposal 1. Proposal 1 keeps its own versioned 8,000-pair fixture, strata, stopping rule, reference values, and evidence."]])
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.article-recap
+ [:strong "Chapter 4 recap"]
+ [:p "Build: declare the 100-pair pool, Beta(1,1) prior, and binary response model. Check: replay the seeded posterior prediction and inspect its interval. Decide: report a finite-pool summary only with its assumptions and uncertainty."]])
+
+;; ## 5. Make the lesson reproducible {#reproducibility}
;;
;; An executable article joins an explanation to calculations that can be run
;; again. **Source code** is the human-readable set of instructions stored in
@@ -713,19 +917,19 @@
;;
;; A **regression check** asks whether an established property still holds
;; after a change. The assertions below protect the 201-point grid, combinatorial
-;; count, normalisation, decision, and positive density. The browser checks add
-;; interaction, console, layout, focus, label, and theme verification. A random
-;; seed makes simulations replayable, so a changed result can be distinguished
-;; from ordinary random variation.
+;; count, normalisation, decision, positive density, Beta update, and finite-pool
+;; prediction. The browser checks add interaction, console, layout, focus,
+;; label, and theme verification. A random seed makes simulations replayable,
+;; so a changed result can be distinguished from ordinary random variation.
^:kindly/hide-code
(math/code-detail
"code-browser-mounts"
- "Mounting all three browser components"
+ "Mounting all four browser components"
[:div
[:p "A mount point is an empty, uniquely identified HTML element reserved for an interactive component. On page load, the browser finds each preserved ID and asks Reagent to render the matching component there."]
- [:pre [:code "(defn ^:export mount []\n (when-let [root (js/document.getElementById\n \"globe-update-simulator\")]\n (rdom/render [globe-update-simulator] root))\n (when-let [root (js/document.getElementById\n \"posterior-sampling-simulator\")]\n (rdom/render [posterior-sampling-simulator] root))\n (when-let [root (js/document.getElementById\n \"gaussian-height-simulator\")]\n (rdom/render [gaussian-height-simulator] root)))"]]
- [:p "The conditional lookup lets the same source load safely even if one mount is absent. The three IDs remain unchanged from the published article."]
+ [:pre [:code "(defn ^:export mount []\n (when-let [root (js/document.getElementById\n \"globe-update-simulator\")]\n (rdom/render [globe-update-simulator] root))\n (when-let [root (js/document.getElementById\n \"posterior-sampling-simulator\")]\n (rdom/render [posterior-sampling-simulator] root))\n (when-let [root (js/document.getElementById\n \"gaussian-height-simulator\")]\n (rdom/render [gaussian-height-simulator] root))\n (when-let [root (js/document.getElementById\n \"vocabulary-pair-simulator\")]\n (rdom/render [vocabulary-bridge-simulator] root)))"]]
+ [:p "The conditional lookup lets the same source load safely even if one mount is absent. The three retained IDs remain unchanged; the fourth is additive."]
[:p.article-code-source [:a {:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive.cljs"} "View the complete mounting and browser code"]]])
^:kindly/hide-code
@@ -735,7 +939,7 @@
[:p "The next article reuses the same structure: candidate knowing rates receive priors; quiz responses supply likelihoods; posteriors predict untested items; seeded draws describe uncertainty; and an explicit stopping decision depends on the intended measurement task."]
[:p [:a {:href "beta_binomial_first_pass.html"} "Continue to the stratified Beta–binomial vocabulary model →"]]])
-;; ## Sources
+;; ## Sources {#sources}
;;
;; - Jamie Pratt and the JointProb group, [original interactive simulations](https://jointprob.github.io/jointprob-shadow-cljs/#normal-distribution) and [ClojureScript source](https://github.com/jointprob/jointprob-shadow-cljs/tree/master/src/cljs).
;; - Richard McElreath, *Statistical Rethinking*, examples from sections 2.2, 3.2, and 4.3. The prompts here are paraphrases.
@@ -754,6 +958,13 @@
1.0e-12))
(assert (= 0.645 (first example-minimum-loss)))
(assert (pos? (normal-density 151.765 155.0 8.0)))
+ (assert (= {:alpha 8.0 :beta 4.0}
+ (:posterior bridge-example-predictive)))
+ (assert (= 67.0 (:mean bridge-example-predictive)))
+ (assert (= [42 89]
+ ((juxt :lower :upper) bridge-example-predictive)))
+ (assert (= 4000
+ (reduce + (vals (:frequencies bridge-example-predictive)))))
(kind/hiccup
[:p.bp-note
[:span.article-marker "Regression check"]
diff --git a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive.cljs b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive.cljs
index 16958f5f..942780f3 100644
--- a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive.cljs
+++ b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive.cljs
@@ -55,6 +55,60 @@
(mapv * prior)
normalize-mean-one)))
+(defn beta-posterior-parameters
+ [{:keys [alpha beta]} {:keys [recognized not-recognized]}]
+ {:alpha (+ alpha recognized)
+ :beta (+ beta not-recognized)})
+
+(defn finite-pool-predictive-mean
+ [pool-size recognized not-recognized alpha beta]
+ (let [remaining (- pool-size recognized not-recognized)]
+ (+ recognized (* remaining (/ alpha (+ alpha beta))))))
+
+(defn integer-gamma-sample! [rng shape]
+ (reduce + (repeatedly (int shape)
+ #(- (js/Math.log
+ (max 1.0e-15 (uniform! rng)))))))
+
+(defn beta-sample! [rng alpha beta]
+ (let [x (integer-gamma-sample! rng alpha)
+ y (integer-gamma-sample! rng beta)]
+ (/ x (+ x y))))
+
+(defn binomial-sample! [rng trials probability]
+ (reduce (fn [successes _]
+ (if (< (uniform! rng) probability)
+ (inc successes)
+ successes))
+ 0
+ (range trials)))
+
+(defn finite-pool-predictive-summary
+ [{:keys [pool-size recognized not-recognized prior-alpha prior-beta
+ draw-count seed]}]
+ (let [remaining (- pool-size recognized not-recognized)
+ {:keys [alpha beta] :as posterior}
+ (beta-posterior-parameters
+ {:alpha prior-alpha :beta prior-beta}
+ {:recognized recognized :not-recognized not-recognized})
+ rng (make-rng seed)
+ totals (vec
+ (repeatedly
+ draw-count
+ (fn []
+ (+ recognized
+ (binomial-sample!
+ rng remaining (beta-sample! rng alpha beta))))))
+ ordered (vec (sort totals))
+ quantile #(nth ordered
+ (js/Math.floor (* % (dec draw-count))))]
+ {:posterior posterior
+ :mean (finite-pool-predictive-mean
+ pool-size recognized not-recognized alpha beta)
+ :lower (quantile 0.025)
+ :upper (quantile 0.975)
+ :frequencies (frequencies totals)}))
+
(def uniform-prior (vec (repeat 201 1.0)))
(def step-up-prior (mapv #(if (<= % 0.5) 0.0 2.0) probability-grid))
(def step-down-prior (mapv #(if (<= % 0.5) 2.0 0.0) probability-grid))
@@ -72,6 +126,55 @@
(def priors-by-label
(into {} (map (juxt :label :values) prior-options)))
+(def vocabulary-pair-fixture
+ [{:lemma "dom" :form "dom" :meaning "house"}
+ {:lemma "być" :form "jest" :meaning "is"}
+ {:lemma "mieć" :form "mam" :meaning "I have"}
+ {:lemma "dziecko" :form "dzieci" :meaning "children"}
+ {:lemma "iść" :form "idzie" :meaning "goes"}
+ {:lemma "dobry" :form "dobra" :meaning "good"}
+ {:lemma "książka" :form "książkę" :meaning "book"}
+ {:lemma "człowiek" :form "ludzie" :meaning "people"}
+ {:lemma "widzieć" :form "widział" :meaning "saw"}
+ {:lemma "duży" :form "większy" :meaning "bigger"}
+ {:lemma "ręka" :form "ręce" :meaning "hand"}
+ {:lemma "wziąć" :form "weźmie" :meaning "will take"}])
+
+(def vocabulary-pool-size 100)
+(def vocabulary-predictive-seed 620260717)
+(def vocabulary-predictive-draw-count 4000)
+(def initial-vocabulary-bridge-state {:responses []})
+(defonce vocabulary-bridge-state (r/atom initial-vocabulary-bridge-state))
+
+(defn vocabulary-response-counts [responses]
+ {:recognized (count (filter #{:recognized} responses))
+ :not-recognized (count (filter #{:not-recognized} responses))})
+
+(defn record-pair-response! [response]
+ (when (and (contains? #{:recognized :not-recognized} response)
+ (< (count (:responses @vocabulary-bridge-state))
+ (count vocabulary-pair-fixture)))
+ (swap! vocabulary-bridge-state update :responses conj response)))
+
+(defn undo-pair-response! []
+ (swap! vocabulary-bridge-state update :responses
+ #(vec (butlast %))))
+
+(defn reset-vocabulary-bridge! []
+ (reset! vocabulary-bridge-state initial-vocabulary-bridge-state))
+
+(defn current-vocabulary-bridge-summary []
+ (let [{:keys [recognized not-recognized]}
+ (vocabulary-response-counts (:responses @vocabulary-bridge-state))]
+ (finite-pool-predictive-summary
+ {:pool-size vocabulary-pool-size
+ :recognized recognized
+ :not-recognized not-recognized
+ :prior-alpha 1
+ :prior-beta 1
+ :draw-count vocabulary-predictive-draw-count
+ :seed vocabulary-predictive-seed})))
+
(def number-formatter (js/Intl.NumberFormat. "en-US"))
(defn format-number [number]
@@ -188,6 +291,132 @@
attributes)
label]))
+(defn finite-pool-predictive-chart
+ [{:keys [mean lower upper frequencies]}]
+ (let [maximum (apply max 1 (vals frequencies))
+ total-x (fn [total] (+ 48 (* 5.5 total)))
+ mean-x (total-x mean)]
+ [:figure.bp-chart
+ [:h4 "Posterior prediction of known pairs"]
+ [:svg {:view-box "0 0 640 285"
+ :role "img"
+ :aria-labelledby "pair-predictive-title pair-predictive-desc"}
+ [:title#pair-predictive-title
+ "Seeded posterior predictive distribution for the 100-pair total"]
+ [:desc#pair-predictive-desc
+ (str "Four thousand seeded predictions. The posterior predictive mean is "
+ (format-decimal mean 1) " known pairs and the central 95 percent interval runs from "
+ lower " to " upper " known pairs.")]
+ [:rect {:x (total-x lower)
+ :y 38
+ :width (max 1 (- (total-x upper) (total-x lower)))
+ :height 182
+ :fill "var(--bp-warm, #a34f00)"
+ :fill-opacity 0.12}]
+ (for [total (range 101)
+ :let [frequency (get frequencies total 0)
+ height (* 176 (/ frequency maximum))]]
+ ^{:key total}
+ [:rect {:x (total-x total)
+ :y (- 220 height)
+ :width 4.7
+ :height height
+ :fill "var(--bp-accent, #1464b5)"
+ :fill-opacity 0.82}])
+ [:line {:x1 mean-x :x2 mean-x :y1 35 :y2 220
+ :stroke "var(--bp-warm, #a34f00)"
+ :stroke-width 3
+ :stroke-dasharray "6 4"}]
+ [:line.bp-axis {:x1 48 :x2 603 :y1 220 :y2 220}]
+ (for [total [0 25 50 75 100]]
+ ^{:key total}
+ [:text {:x (total-x total) :y 242 :text-anchor "middle"
+ :font-size 11 :fill "currentColor"}
+ total])
+ [:text {:x 326 :y 273 :text-anchor "middle" :font-size 12
+ :fill "currentColor"}
+ "Known lemma–surface-form pairs out of 100"]]
+ [:p.bp-caption
+ (str "Blue bars: seeded predictive frequency. Shaded band: central 95% interval "
+ lower "–" upper ". Dashed line: mean " (format-decimal mean 1) ".")]]))
+
+(defn vocabulary-bridge-simulator []
+ (let [responses (:responses @vocabulary-bridge-state)
+ response-count (count responses)
+ {:keys [recognized not-recognized]}
+ (vocabulary-response-counts responses)
+ {:keys [posterior mean lower upper] :as summary}
+ (current-vocabulary-bridge-summary)
+ next-pair (nth vocabulary-pair-fixture response-count nil)
+ completed? (= response-count (count vocabulary-pair-fixture))]
+ [:section.bp-shell {:aria-labelledby "vocabulary-bridge-heading"}
+ [:h3#vocabulary-bridge-heading "Predict a finite 100-pair total"]
+ [:p
+ "Treat each answer as binary evidence about one lemma–surface-form pair. The fixture is deliberately small and transparent: it is a teaching model, not a calibrated vocabulary test."]
+ [:div.bp-process-strip
+ {:role "img"
+ :aria-label
+ (str "Beta one one prior updated by " recognized " recognized and "
+ not-recognized " not-recognized responses to form a Beta "
+ (:alpha posterior) " " (:beta posterior)
+ " posterior, then predict the finite total among 100 pairs.")}
+ [:div.bp-process-step [:strong "Prior"] [:small "Beta(1,1)"]]
+ [:div.bp-process-symbol {:aria-hidden "true"} "×"]
+ [:div.bp-process-step
+ [:strong "Likelihood"]
+ [:small (str recognized " recognized · " not-recognized " not")]]
+ [:div.bp-process-symbol {:aria-hidden "true"} "="]
+ [:div.bp-process-step
+ [:strong "Posterior"]
+ [:small (str "Beta(" (:alpha posterior) "," (:beta posterior) ")")]]
+ [:div.bp-process-symbol {:aria-label "then predict"} "→"]
+ [:div.bp-process-step
+ [:strong "100-pair prediction"]
+ [:small (str (format-decimal mean 1) " · 95% " lower "–" upper)]]]
+ [:div.bp-pair-prompt
+ (if next-pair
+ [:div
+ [:p.bp-stat
+ [:strong (str "Fixture pair " (inc response-count) " of "
+ (count vocabulary-pair-fixture))]]
+ [:p
+ "Would you recognise "
+ [:strong (:form next-pair)]
+ " as a form of "
+ [:strong (:lemma next-pair)]
+ " with the intended meaning “" (:meaning next-pair) "”? "]]
+ [:p [:strong "Fixture complete."]
+ " Undo or reset to explore another response path."])]
+ [:div.bp-controls
+ [control-button "Recognized"
+ #(record-pair-response! :recognized)
+ {:class "bp-button bp-primary" :disabled completed?}]
+ [control-button "Not recognized"
+ #(record-pair-response! :not-recognized)
+ {:disabled completed?}]
+ [control-button "Undo" undo-pair-response!
+ {:disabled (zero? response-count)}]
+ [control-button "Reset" reset-vocabulary-bridge!
+ {:disabled (zero? response-count)}]]
+ [:p.bp-stat {:aria-live "polite"}
+ (str response-count " responses: " recognized " recognized, "
+ not-recognized " not recognized. Posterior Beta("
+ (:alpha posterior) ", " (:beta posterior) "). Predicted mean "
+ (format-decimal mean 1) " known pairs out of 100; central 95% interval "
+ lower " to " upper ".")]
+ [finite-pool-predictive-chart summary]
+ [:details.bp-details
+ [:summary "What this teaching model assumes"]
+ [:div
+ [:ul
+ [:li "The finite pool contains exactly 100 versioned pairs."]
+ [:li "The asked pairs are treated as exchangeable observations from that pool."]
+ [:li "Recognized and not recognized are treated as error-free binary evidence."]
+ [:li "A seeded Beta draw supplies a knowing rate; a seeded binomial draw predicts knowledge among the unasked pairs."]]
+ [:p "Real items can be guessed, misunderstood, or differently difficult. Those omissions are boundaries, not evidence that the model is learner-valid."]]]
+ [:p.bp-note
+ "Posterior-predictive seed 620260717 · 4,000 draws · Reset plus the same responses reproduces the same distribution."]]))
+
;; Simulation 1: sequential globe observations.
(def max-update-samples 200)
@@ -844,13 +1073,17 @@
[:p.bp-note "Each map contains all 1,681 (μ, σ) candidates. Colour opacity uses a logarithmic scale within that panel; compare location and concentration, not absolute colour between panels."]]))
(defn ^:export mount []
- (when-let [root (js/document.getElementById "globe-update-simulator")]
- (rdom/render [globe-update-simulator] root))
- (when-let [root (js/document.getElementById "posterior-sampling-simulator")]
- (rdom/render [posterior-sampling-simulator] root))
- (when-let [root (js/document.getElementById "gaussian-height-simulator")]
- (rdom/render [gaussian-height-simulator] root)))
-
-(if (= "loading" js/document.readyState)
- (.addEventListener js/document "DOMContentLoaded" mount)
- (mount))
+ (when (exists? js/document)
+ (when-let [root (js/document.getElementById "globe-update-simulator")]
+ (rdom/render [globe-update-simulator] root))
+ (when-let [root (js/document.getElementById "posterior-sampling-simulator")]
+ (rdom/render [posterior-sampling-simulator] root))
+ (when-let [root (js/document.getElementById "gaussian-height-simulator")]
+ (rdom/render [gaussian-height-simulator] root))
+ (when-let [root (js/document.getElementById "vocabulary-pair-simulator")]
+ (rdom/render [vocabulary-bridge-simulator] root))))
+
+(when (exists? js/document)
+ (if (= "loading" js/document.readyState)
+ (.addEventListener js/document "DOMContentLoaded" mount)
+ (mount)))
diff --git a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_preview.png b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_preview.png
index c023ac73..b2130967 100644
Binary files a/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_preview.png and b/src/language_learning/vocabulary_estimation/bayes_theorem_simulations_preview.png differ
diff --git a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj
index f2d4953e..456bc668 100644
--- a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj
+++ b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj
@@ -1,13 +1,14 @@
^{:kindly/hide-code true
:kindly/options {:html/deps [:scittle :reagent]}
:clay {:hide-info-line true
- :title "Estimating Vocabulary Size: A Stratified Beta–Binomial First Pass"
+ :title "Proposal 1: estimating known pairs"
:quarto {:author :jamiep
- :description "A learning-in-public first pass at estimating receptive vocabulary from stratified responses with a Beta–binomial model."
+ :subtitle "One seeded non-adaptive learner attempt, from an 8,000-pair pool to a stopping decision"
+ :description "Follow one seeded learner attempt through balanced selection, three-way response storage, stratified Beta–binomial prediction, and stopping."
:type :post
:date "2026-07-12"
:image "beta_binomial_first_pass_preview.png"
- :image-alt "Beta-binomial posterior density after twelve vocabulary responses, with Correct, Wrong, Do not know, and Reset controls."
+ :image-alt "Proposal 1 article title and subtitle introduce one seeded non-adaptive learner attempt, with Article 4 of 5 series navigation."
:category :concepts
:tags [:bayesian-statistics :language-learning :clojure :scittle]
:keywords [:vocabulary-estimation :beta-binomial :posterior-predictive :stratified-sampling]}}}
@@ -41,67 +42,83 @@
(kind/hiccup
[:style (resource-text "beta_binomial_first_pass_series.css")])
+^:kindly/hide-code
+(controls/install
+ {:article-id :proposal-1
+ :sections [{:id "attempt-target" :label "Define the pair-count target"}
+ {:id "attempt-selection" :label "Select one balanced schedule"}
+ {:id "attempt-responses" :label "Preserve three-way responses"}
+ {:id "attempt-update" :label "Update each stratum"}
+ {:id "attempt-prediction" :label "Predict the finite pool"}
+ {:id "attempt-stopping" :label "Decide when to stop"}]
+ :technical-sections [{:id "attempt-replay" :label "Replay the versioned evidence"}
+ {:id "sources" :label "Sources and further reading"}]})
+
+;; This article follows **one seeded synthetic learner attempt** from the first
+;; selection decision to the stopping decision. The learner is fictional; the
+;; fixed events make the explanation concrete and exactly replayable. Each
+;; stage answers the question that arises next in that same attempt.
+;;
+;; I call this model **Proposal 1**. Its exact algorithm ID remains
+;; `stratified-beta-binomial-v1`, with version 1 and seed 20260712. Proposal 1
+;; is the **current research implementation target** after Proposal 2 was not
+;; promoted. It is **not the scorer currently deployed at Lexibench**.
+
^:kindly/hide-code
(kind/hiccup
- [:nav.series-toc {:aria-labelledby "series-contents-heading"}
- [:h2#series-contents-heading "Theory to vocabulary-estimation series"]
- [:p "Article 0 explains the workflow; this article applies the previous Bayesian tools to one deliberately narrow measurement problem."]
- [:ol {:start 0}
- [:li [:a {:href "managing_brilliant_but_uneven_minds.html"}
- "Managing brilliant but uneven minds: my theory-to-algorithm workflow"]
- [:span.series-status "published"]]
- [:li [:a {:href "bayes_theorem_simulations.html"}
- "Bayes' theorem from uncertainty to decision"]
- [:span.series-status "published"]]
- [:li.series-current [:a {:href "beta_binomial_first_pass.html"
- :aria-current "page"}
- "Estimating vocabulary size with a simple Bayesian model"]
- [:span.series-status "you are here"]]
- [:li [:a {:href "pair_frequency_logistic_v2_article.html"}
- "Does pair frequency predict learner responses?"]
- [:span.series-status "published"]]
- [:li "From Self-Reported CEFR to a Versioned Lemma–Form-Pair Pool"
- [:span.series-status "planned"]]
- [:li "From Correlated Form Pairs to Latent Lemma Knowledge"
- [:span.series-status "planned"]]
- [:li "Modelling Correct, Wrong, and Don't-Know Separately"
- [:span.series-status "planned"]]
- [:li "Calibrating Items Before IRT and Adaptive Selection"
- [:span.series-status "planned"]]
- [:li "When Contexts and Senses Become Identifiable"
- [:span.series-status "planned"]]]])
-
-^:kindly/hide-code
-(controls/install)
-
-;; A test result is meaningful only after its target is defined. **Measurement**
-;; is the disciplined act of connecting recorded observations to that target.
-;; It is not the same as attaching a number to a person.
-;;
-;; I am building [**Lexibench**](https://lexibench.com/), but the model in this
-;; article is a historical first-pass target scorer. It is **not a description
-;; of the scorer currently deployed at Lexibench**. Correct probability
-;; calculations cannot rescue a vague target or an unsuitable item pool.
+ [:div.ve-attempt-brief
+ [:div [:span "Attempt"] [:strong "proposal-1-seeded-attempt-2026-07-12"]]
+ [:div [:span "Pool"] [:strong "8,000 lemma–surface-form pairs"]]
+ [:div [:span "Schedule"] [:strong "4 rounds × 8 strata"]]
+ [:div [:span "Reference result"] [:strong "4,334 pairs · 95% interval 3,404–5,249"]]])
+
+^:kindly/hide-code
+(def proposal-1-configuration
+ {:algorithm-id :stratified-beta-binomial-v1
+ :algorithm-version 1
+ :strata-count 8
+ :prior {:alpha 1.0 :beta 1.0}
+ :credible-mass 0.95
+ :minimum-items 32
+ :round-size 8
+ :target-half-width-ratio 0.10
+ :soft-maximum-items 96
+ :posterior-draws 50000
+ :seed 20260712})
+
+^:kindly/hide-code
+(math/code-detail
+ "code-proposal-1-configuration"
+ "The versioned Proposal 1 configuration"
+ [:div
+ [:p "The display name does not replace the machine contract. Replay requires every value below, including the seed and draw count."]
+ [:pre [:code (pr-str proposal-1-configuration)]]
+ [:p.article-code-source
+ [:a {:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj"}
+ "View the complete executable article source"]]])
^:kindly/hide-code
(kind/hiccup
[:ol.article-chapter-map
- [:li [:strong "Define"] [:br] "What exactly would the reported count mean?"]
- [:li [:strong "Sample"] [:br] "How can eight balanced strata represent a fixed pool?"]
- [:li [:strong "Update"] [:br] "How do responses change uncertainty within each stratum?"]
- [:li [:strong "Predict"] [:br] "How do tested and untested pairs combine into a finite total?"]
- [:li [:strong "Decide"] [:br] "When is the estimate precise enough to stop?"]
- [:li [:strong "Reproduce"] [:br] "How do fixtures, seeds, tests, and publication gates protect the result?"]])
-
-;; ## 1. Define: what is being measured?
+ [:li [:strong "Target"] [:br] "What will this attempt estimate?"]
+ [:li [:strong "Select"] [:br] "Which 32 pairs will the learner see?"]
+ [:li [:strong "Record"] [:br] "What must be preserved before scoring?"]
+ [:li [:strong "Update"] [:br] "What did the four rounds say within each stratum?"]
+ [:li [:strong "Predict"] [:br] "What might be known among the untested pairs?"]
+ [:li [:strong "Stop"] [:br] "Is the result precise enough to end the attempt?"]
+ [:li [:strong "Replay"] [:br] "Can the same versioned evidence reproduce it?"]])
+
+;; ## 1. Define this attempt's target {#attempt-target}
;;
-;; The casual question “How many words do you know?” hides several choices.
+;; Before selecting the learner's first item, Proposal 1 needs a countable
+;; target. The casual question “How many words do you know?” hides several choices.
;; Does *ran* count separately from *run*? Does one meaning of *bank* count
;; separately from another? Which language inventory supplies the denominator?
-;; This first pass cannot answer every version of that question. It answers one
+;; Proposal 1 cannot answer every version of that question. It answers one
;; narrower, reproducible version.
;;
-;; The estimand here is:
+;; The estimated quantity is receptive knowledge of lemma–surface-form pairs
+;; in one fixed, versioned pool. In compact form, the estimand is:
;;
;; > **Receptive knowledge of lemma–surface-form pairs in a fixed, versioned
;; > pool.**
@@ -168,12 +185,14 @@
[:strong "Provisional modelling choice"]
"Provisional means chosen as a testable starting assumption, not established by the data. The pair inventory, canonical contexts, frequency strata, response mapping, prior, and stopping threshold all need empirical validation."])
-;; ## 2. Sample: balanced rounds from a fixed pool
+;; ## 2. Select one broad, fixed schedule {#attempt-selection}
;;
-;; For exposition, imagine a fixed, versioned pool split by frequency rank into eight
-;; strata with the same number of pairs. A round samples one item uniformly at
-;; random from each stratum. Later answers do not change which item is selected:
-;; **selection remains non-adaptive**.
+;; The attempt now needs a short sample that still spans the pool. Its synthetic,
+;; fixed, versioned 8,000-pair pool is split by frequency rank into eight strata
+;; of 1,000 pairs. Proposal 1 seeds and stores one shuffled queue per stratum.
+;; Each round takes the next unseen item from every queue, then shuffles those
+;; eight items for presentation. Later answers do not change any queue:
+;; **selection remains broad, balanced, and non-adaptive**.
;;
;; The full pool is the **population** about which this test makes its claim. The
;; much smaller set of administered items is the **sample**. Sampling is needed
@@ -201,11 +220,12 @@
;; that look much too easy or much too hard. That can shorten a test, but only if
;; the item-difficulty model is trustworthy.
;;
-;; I therefore want to keep selection non-adaptive while collecting response
+;; I therefore keep this attempt non-adaptive while collecting response
;; data from real learners. Those data should reveal the relative difficulty of
;; the actual questions—including context effects and departures from frequency
-;; rank—before I implement an adaptive test. Adapting sooner would risk steering
-;; the test with assumptions that have not yet been checked against learners.
+;; rank—before I implement an adaptive test. Adaptive selection is explicitly
+;; later work after item calibration; adapting sooner would steer the test with assumptions
+;; that have not yet been checked against learners.
^:kindly/hide-code
(kind/mermaid
@@ -232,17 +252,18 @@
[:p.ve-caption
"Frequency rank partitions the illustrative pool; this diagram does not claim that rank is a calibrated item-difficulty scale."])
-;; **Frequency rank** is only a proxy: a convenient observable used in place of
+;; In this design, **frequency rank is a proxy**: a convenient observable used in place of
;; an unobserved quantity. Here it stands in for item difficulty, but it has not
;; been calibrated against learner responses. Equal-count strata balance the
;; sample across that proxy; they do not prove equal difficulty inside a band.
;;
;; ### Watch the fixed schedule
;;
-;; Select items one at a time below. Read across the eight cards: each round
-;; takes exactly one unseen item from every stratum. The sample responses are
-;; shown, but the “next item” labels advance according to the pre-existing
-;; queues, never according to those responses.
+;; Reveal the seeded attempt one item at a time below. Read across the eight
+;; cards: each of the four rounds takes exactly one unseen item from every
+;; stratum. The synthetic responses are shown, but the “next item” labels
+;; advance according to the pre-existing queues, never according to those
+;; responses.
^:kindly/hide-code
(kind/hiccup
@@ -257,14 +278,61 @@
[:span.article-marker "Build / Check / Decide"]
[:p "Build: version a finite population and queue unseen items in eight frequency bands. Check: every completed round contains one item from each band, with no repeats. Decide: keep selection non-adaptive until a learner-calibrated difficulty model exists."]])
-;; ## 3. Record first, collapse only for v1 inference
+;; ## 3. Preserve the attempt before scoring {#attempt-responses}
;;
-;; The interface keeps three raw response values:
-;; `:correct`, `:wrong`, and `:dont-know`. Stage one maps a correct response to
+;; After each item, the attempt appends a complete event. The interface keeps
+;; three raw response values:
+;; `:correct`, `:wrong`, and `:dont-know`. Proposal 1 maps a correct response to
;; “known” and both other responses to “not known.” Keeping the raw outcomes
;; distinct lets a later model treat guessing, slips, and explicit uncertainty
;; differently without corrupting the original data.
+^:kindly/hide-code
+(def seeded-attempt-responses-by-stratum
+ [[:correct :correct :correct :correct]
+ [:correct :correct :correct :correct]
+ [:correct :correct :correct :wrong]
+ [:correct :correct :correct :dont-know]
+ [:correct :correct :wrong :dont-know]
+ [:correct :wrong :dont-know :wrong]
+ [:correct :dont-know :wrong :dont-know]
+ [:wrong :dont-know :wrong :dont-know]])
+
+^:kindly/hide-code
+(def seeded-stratum-orders
+ [[3 8 1 6 4 2 7 5]
+ [5 2 7 1 8 4 3 6]
+ [8 4 6 2 7 3 5 1]
+ [2 6 1 7 3 5 8 4]])
+
+^:kindly/hide-code
+(def seeded-attempt-events
+ (let [attempt-start (java.time.Instant/parse "2026-07-12T12:00:00Z")]
+ (vec
+ (for [round-index (range 4)
+ [position stratum-number]
+ (map-indexed vector (nth seeded-stratum-orders round-index))
+ :let [stratum-index (dec stratum-number)
+ event-index (+ (* round-index 8) position)
+ response (get-in seeded-attempt-responses-by-stratum
+ [stratum-index round-index])]]
+ {:attempt-id "proposal-1-seeded-attempt-2026-07-12"
+ :algorithm-id :stratified-beta-binomial-v1
+ :algorithm-version 1
+ :pool-id "pedagogical-8000-v1"
+ :pool-version "2026-07-12"
+ :item-version-id (str "item-s" stratum-number
+ "-r" (inc round-index) "-v1")
+ :pair-id [(str "lemma-s" stratum-number "-r" (inc round-index))
+ (str "form-s" stratum-number "-r" (inc round-index))]
+ :stratum-index stratum-index
+ :round-index round-index
+ :position position
+ :response response
+ :response-ms (+ 1800 (* 37 event-index))
+ :presented-at (.plusSeconds attempt-start (* 5 event-index))
+ :selection-probability (/ 1 (- 1000 round-index))}))))
+
^:kindly/hide-code
(defn collapse-response
"Map a raw response to the binary stage-one likelihood outcome."
@@ -284,7 +352,23 @@
{:k (reduce + (map collapse-response responses))
:n (count responses)})
-;; The mapping keeps `:wrong` and `:dont-know` as different raw keys even though
+^:kindly/hide-code
+(defn attempt-events->strata
+ "Replay raw attempt events into Proposal 1's eight sufficient-statistic rows."
+ [events]
+ (mapv
+ (fn [stratum-index]
+ (let [responses (->> events
+ (filter #(= stratum-index (:stratum-index %)))
+ (sort-by :round-index)
+ (mapv :response))]
+ (assoc (collapse-responses responses) :pool-size 1000)))
+ (range 8)))
+
+;; The 32 stored events contain 18 `:correct`, 7 `:wrong`, and 7 `:dont-know`
+;; values. In event-shaped prose, those are `:response :correct`,
+;; `:response :wrong`, and `:response :dont-know`. The mapping keeps `:wrong`
+;; and `:dont-know` as different raw keys even though
;; their stage-one likelihood contribution is the same.
;;
;; A **Bernoulli trial** has two outcomes for this model: 1 for correct and 0
@@ -297,21 +381,23 @@
"code-raw-response-mapping"
"Mapping raw responses without discarding the events"
[:div
- [:p "The pure function returns the v1 binary outcome. Unknown values fail loudly instead of silently entering the model."]
+ [:p "The pure function returns Proposal 1's binary outcome. Unknown values fail loudly instead of silently entering the model."]
[:pre [:code "(defn collapse-response [response]\n (case response\n :correct 1\n :wrong 0\n :dont-know 0\n (throw (ex-info \"Unknown raw response\"\n {:response response}))))"]]
[:p "Storage retains the original keyword; only the input to this first inference model is collapsed."]
[:p.article-code-source [:a {:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj"} "View the executable article source"]]])
-;; ## 4. Update: Beta in, Beta out within one stratum
+;; ## 4. Update each stratum after four rounds {#attempt-update}
;;
-;; A sequence of Bernoulli trials can be summarized by a **binomial count**:
+;; With four events now stored in every stratum, Proposal 1 derives—not
+;; overwrites—a binary likelihood view. A sequence of Bernoulli trials can be
+;; summarized by a **binomial count**:
;; $k$ correct responses among $n$ attempts. A **Beta distribution** describes
;; uncertainty about a probability between 0 and 1. It has two positive shape
;; parameters, $\alpha$ and $\beta$. Choosing a Beta prior makes the update
;; **conjugate**: after binomial data, the posterior is another Beta
;; distribution, so no numerical approximation is needed for this step.
;;
-;; Let $p_s$ be the knowing rate in stratum $s$. This first pass gives every
+;; Let $p_s$ be the knowing rate in stratum $s$. Proposal 1 gives every
;; stratum the same prior:
;;
;; $$p_s \sim \operatorname{Beta}(1,1).$$
@@ -325,7 +411,7 @@
["∼" "“Is distributed as.” It describes uncertainty about p_s, not an equality to one number."]
["Beta(1,1)" "A Beta distribution with shape parameters α = 1 and β = 1. Its density is uniform from 0 to 1."]
["α, β" "The Beta distribution’s two positive shape parameters. In this Bernoulli model they update like prior correct and not-correct counts."]]
- "Beta(1,1) is the deliberately simple v1 prior; calling it uniform does not make it universally uninformative.")
+ "Beta(1,1) is Proposal 1's deliberately simple prior; calling it uniform does not make it universally uninformative.")
^:kindly/hide-code
(math/equation-code-detail
@@ -362,7 +448,7 @@
["|" "“Given” or “conditional on.” Everything to its right is treated as observed information."]
["k" "The number of correct responses in this stratum."]
["n" "The total number of tested pairs in this stratum."]
- ["n − k" "The number of tested responses treated as not known by the v1 inference model."]
+ ["n − k" "The number of tested responses treated as not known by Proposal 1's inference model."]
["1 + k" "The posterior α parameter: prior α = 1 plus correct responses."]
["1 + n − k" "The posterior β parameter: prior β = 1 plus not-correct responses."]
["Beta(…)" "The posterior stays in the Beta family because the Beta prior is conjugate to the Bernoulli/binomial likelihood."]]
@@ -384,8 +470,8 @@
{:kind :source
:label "beta_binomial_first_pass.clj — posterior-parameters"
:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj"
- :symbols [["alpha" "The code name for the prior α shape; 1.0 in v1."]
- ["beta" "The code name for the prior β shape; 1.0 in v1."]
+ :symbols [["alpha" "The code name for the prior α shape; 1.0 in Proposal 1."]
+ ["beta" "The code name for the prior β shape; 1.0 in Proposal 1."]
["k" "The correct count k added to alpha."]
["n" "The attempt count n; n - k is added to beta."]]}
[:div
@@ -399,12 +485,13 @@
;; updated density for $p_s$. The “likelihood” is the information supplied by
;; the last response as a function of $p_s$; it is not itself a posterior.
;;
-;; ### Try the update
+;; ### Replay one stratum's update
;;
-;; Press **Correct**, **Wrong**, or **Don't know**. The accessible SVG compares the uniform
-;; prior, the previous posterior, the last-response likelihood, and the current
-;; posterior. Curve height is **density**, not the probability of one exact
-;; value of $p$.
+;; The recorded attempt has four responses per stratum. Use **Correct**,
+;; **Wrong**, or **Don't know** below to replay any one of those four-response
+;; paths. The accessible SVG compares the uniform prior, previous posterior,
+;; last-response likelihood, and current posterior. Curve height is **density**,
+;; not the probability of one exact value of $p$.
^:kindly/hide-code
(kind/hiccup
@@ -422,10 +509,12 @@
[:p "Loading the Bayesian update simulator…"]]
[:noscript "This simulator needs JavaScript. The statistical core above remains readable without it."]])
-;; ## 5. Predict: from knowing rates to an untested finite pool
+;; ## 5. Predict what remains in the finite pool {#attempt-prediction}
;;
-;; For a stratum containing $N_s$ pairs, of which $n_s$ were tested and $k_s$
-;; were correct:
+;; Four rounds tell us only 32 of 8,000 outcomes. Proposal 1 must now predict
+;; the finite untested remainder without predicting those tested outcomes a
+;; second time. For a stratum containing $N_s$ pairs, of which $n_s$ were tested
+;; and $k_s$ were correct:
;;
;; 1. draw $p_s$ from the posterior;
;; 2. draw the number known among the untested pairs from
@@ -527,19 +616,19 @@
[:p "This function selects quantiles from Monte Carlo draws. The exact reference interval below instead selects where the cumulative exact discrete probability crosses the same two cut points."]
[:p.article-code-source [:a {:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj"} "View the quantile implementation"]]])
-;; ## 6. Combine: a pedagogical 8,000-pair pool
+;; ## 6. Combine this attempt's eight strata
;;
;; **8,000 is an illustration, not a current CEFR or Lexibench pool size.** It
-;; consists of eight 1,000-pair strata. After four complete rounds, suppose the
-;; correct counts are `[4 4 3 3 2 1 1 0]`.
+;; consists of eight 1,000-pair strata. Replaying the 32 raw events after four
+;; complete rounds gives correct counts `[4 4 3 3 2 1 1 0]`.
^:kindly/hide-code
-(def worked-correct-counts [4 4 3 3 2 1 1 0])
+(def worked-strata
+ (attempt-events->strata seeded-attempt-events))
^:kindly/hide-code
-(def worked-strata
- (mapv (fn [k] {:pool-size 1000 :k k :n 4})
- worked-correct-counts))
+(def worked-correct-counts
+ (mapv :k worked-strata))
^:kindly/hide-code
(defn analytic-stratum-mean
@@ -563,7 +652,7 @@
(long (reduce + (map analytic-stratum-mean worked-strata))))
;; The **mean** is the probability-weighted average of all possible totals under
-;; the model. It is **4,334 pairs** here. Row by row:
+;; the model. For this attempt it is **4,334 pairs**. Row by row:
^:kindly/hide-code
(kind/hiccup
@@ -638,8 +727,9 @@
;;
;; Reader-facing, I would report:
;;
-;; > **About 4,330 known pairs, with a 95% credible interval of roughly
-;; > 3,400–5,250, conditional on this model and fixed pool.**
+;; > **Estimated receptive knowledge: about 4,330 lemma–surface-form pairs in
+;; > synthetic pool `pedagogical-8000-v1`, version `2026-07-12`, with a 95%
+;; > credible interval of roughly 3,400–5,250.**
;;
;; “95% credible interval” is not “95% certainty that every modelling choice is
;; right.” It describes posterior uncertainty conditional on the specified
@@ -736,9 +826,12 @@
;; Small differences are Monte Carlo error. The verification tolerance used for
;; this draft is ±20 pairs for the mean and ±40 pairs for each endpoint.
;;
-;; ## 7. Decide: when should the test stop?
+;; ## 7. Decide whether this attempt should stop {#attempt-stopping}
;;
-;; The stopping rule is also provisional:
+;; At the first eligible check, the attempt has 32 items and the exact reference
+;; interval 3,404–5,249. Its half-width is 922.5 pairs, wider than the target of
+;; 800, so Proposal 1 recommends **continue**. The learner may still stop
+;; voluntarily and receive the qualified result. The rule is provisional:
;;
;; - ask at least 32 items (four complete rounds);
;; - reassess only after another complete eight-item round;
@@ -775,7 +868,7 @@
^:kindly/hide-code
(math/code-detail
"code-stopping-decision"
- "Applying the v1 stopping decision"
+ "Applying the Proposal 1 stopping decision"
[:div
[:p "Assessment occurs only at complete eight-item rounds and only after the 32-item minimum. The precision target and soft maximum are separate reasons to recommend stopping."]
[:pre [:code "(let [complete-round? (zero? (mod items-tested 8))\n assess? (and complete-round?\n (>= items-tested 32))\n half-width (/ (- upper lower) 2.0)\n target? (and assess?\n (<= half-width (* 0.10 pool-size)))\n soft-max? (and assess?\n (>= items-tested 96))]\n {:stop? (or voluntary? target? soft-max?)})"]]
@@ -812,13 +905,14 @@
[:td (format "%,.0f" half-width)]
[:td (if stop? "Yes" "No")]])]]])
-;; At 36 items the interval happens to be narrow enough, but the function does
+;; The 32-item row is this seeded attempt's first stopping decision. At 36 items
+;; the interval happens to be narrow enough, but the function does
;; not assess it: 36 is not the end of an eight-item round. At 96, the soft
;; maximum recommends stopping even when the width target is missed.
;; ### Explore the rule without rewriting history
;;
-;; The controls below begin at the authoritative v1 defaults: minimum 32,
+;; The controls below begin at the authoritative Proposal 1 defaults: minimum 32,
;; interval half-width target 10% of the pool, and soft cap 96. Changing those
;; three values creates a **counterfactual teaching scenario** only. It does not
;; alter this article's recorded model, examples, assertions, or scorer.
@@ -828,7 +922,7 @@
[:div.ve-simulator
[:div#stopping-rule-explorer
[:p "Loading the teaching-only stopping-rule explorer…"]]
- [:noscript "This explorer needs JavaScript. The authoritative v1 defaults and stopping table above remain available."]])
+ [:noscript "This explorer needs JavaScript. The authoritative Proposal 1 defaults and stopping table above remain available."]])
^:kindly/hide-code
(kind/hiccup
@@ -836,9 +930,10 @@
[:span.article-marker "Build / Check / Decide"]
[:p "Build: compute the exact finite-pool distribution and a qualified estimate. Check: reproduce it with a seeded simulation and inspect stopping only at round boundaries. Decide: stop for voluntary choice, adequate precision after the minimum, or the soft maximum—without changing the rule after seeing one learner's result."]])
;;
-;; ## 8. Reproduce: protect this model's evidence
+;; ## 8. Replay the versioned attempt {#attempt-replay}
;;
-;; A trustworthy calculation needs a boundary between the mathematical model
+;; The learner story ends only when another implementation can replay it. A
+;; trustworthy calculation needs a boundary between the mathematical model
;; and software that reads files, draws charts, or responds to clicks. A **pure
;; function** returns an output determined only by its inputs. A **side effect**
;; changes or observes something outside that return value, such as a database,
@@ -858,24 +953,30 @@
(kind/hiccup
[:div.ve-callout
[:strong "The wider workflow lives in Article 0"]
- [:p "This section keeps the evidence specific to v1: pure scoring, immutable fixtures and response events, seeded replay, and CLJ/CLJS parity. "
+ [:p "This section keeps the evidence specific to Proposal 1: pure scoring, immutable fixtures and response events, seeded replay, and CLJ/CLJS parity. "
[:a {:href "managing_brilliant_but_uneven_minds.html#the-research-cycle-in-public"}
"Article 0 explains the complete theory-to-algorithm cycle and its separate model, software, and publication gates."]]])
^:kindly/hide-code
(def worked-fixture
{:fixture-version "beta-binomial-v1-worked-2026-07-12"
- :algorithm-version "beta-binomial-v1"
+ :algorithm-id :stratified-beta-binomial-v1
+ :algorithm-version 1
:pool-id "pedagogical-8000-v1"
+ :pool-version "2026-07-12"
:seed 20260712
+ :raw-responses seeded-attempt-events
:strata worked-strata})
^:kindly/hide-code
(defn score-worked-fixture
"Deterministic scoring shell for the versioned pedagogical fixture."
- [{:keys [seed strata]}]
- {:analytic-mean (long (reduce + (map analytic-stratum-mean strata)))
- :one-seeded-draw (posterior-predictive-total seed strata)})
+ [{:keys [seed strata raw-responses]}]
+ (let [replayed-strata (if (seq raw-responses)
+ (attempt-events->strata raw-responses)
+ strata)]
+ {:analytic-mean (long (reduce + (map analytic-stratum-mean replayed-strata)))
+ :one-seeded-draw (posterior-predictive-total seed replayed-strata)}))
^:kindly/hide-code
(math/code-detail
@@ -883,7 +984,7 @@
"Keeping a pure scoring boundary and deterministic fixture"
[:div
[:p "All changing inputs are explicit data. The function creates its seeded generator internally, returns a value, and neither reads nor writes application storage."]
- [:pre [:code "(def worked-fixture\n {:fixture-version \"beta-binomial-v1-worked-2026-07-12\"\n :algorithm-version \"beta-binomial-v1\"\n :pool-id \"pedagogical-8000-v1\"\n :seed 20260712\n :strata worked-strata})\n\n(defn score-worked-fixture [{:keys [seed strata]}]\n {:analytic-mean\n (long (reduce + (map analytic-stratum-mean strata)))\n :one-seeded-draw\n (posterior-predictive-total seed strata)})"]]
+ [:pre [:code "(def worked-fixture\n {:fixture-version \"beta-binomial-v1-worked-2026-07-12\"\n :algorithm-id :stratified-beta-binomial-v1\n :algorithm-version 1\n :pool-id \"pedagogical-8000-v1\"\n :pool-version \"2026-07-12\"\n :seed 20260712\n :raw-responses seeded-attempt-events})\n\n(defn score-worked-fixture [{:keys [seed raw-responses]}]\n (let [strata (attempt-events->strata raw-responses)]\n {:analytic-mean\n (long (reduce + (map analytic-stratum-mean strata)))\n :one-seeded-draw\n (posterior-predictive-total seed strata)}))"]]
[:p "Calling this function twice with the same fixture must return equal maps; the final regression checks enforce that deterministic replay."]
[:p.article-code-source [:a {:href "https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj"} "View the fixture, scorer, and assertions"]]])
@@ -915,9 +1016,9 @@
(kind/hiccup
[:div.article-recap
[:span.article-marker "Checkpoint"]
- [:p "The v1 model now has a precise estimand, balanced non-adaptive sampling, lossless raw events, eight conjugate Beta updates, finite-pool posterior prediction, an exact qualified interval, seeded numerical checks, and an explicit stopping rule. Every modelling choice remains provisional. The next article tests one refinement—continuous pair frequency—without silently rewriting this checkpoint."]])
+ [:p "Proposal 1 now has a precise pair-count estimand, balanced non-adaptive sampling, lossless raw events, eight conjugate Beta updates, finite-pool posterior prediction, an exact qualified interval, seeded numerical checks, and an explicit stopping rule. It remains the current research implementation target, and every modelling choice remains provisional. The next article tests one refinement—continuous pair frequency—without silently rewriting this checkpoint."]])
;;
-;; ## Sources and further reading
+;; ## Sources and further reading {#sources}
;;
;; - Gelman et al., [*Bayesian Data Analysis*](https://sites.stat.columbia.edu/gelman/book/), for posterior and posterior-predictive reasoning.
;; - [Fastmath random-distribution documentation](https://generateme.github.io/fastmath/fastmath.random.html), for the executable Beta, binomial, sampling, and inverse-CDF interfaces used here.
@@ -934,11 +1035,38 @@
^:kindly/hide-code
(def regression-status
(do
+ (assert (= 32 (count seeded-attempt-events)))
+ (assert (= [4 4 3 3 2 1 1 0] worked-correct-counts))
+ (assert (= {:correct 18 :wrong 7 :dont-know 7}
+ (frequencies (map :response seeded-attempt-events))))
+ (assert (= 32 (count (distinct (map :item-version-id
+ seeded-attempt-events)))))
+ (assert (every? #(instance? java.time.Instant (:presented-at %))
+ seeded-attempt-events))
+ (assert (= seeded-stratum-orders
+ (mapv (fn [round-index]
+ (mapv (comp inc :stratum-index)
+ (filter #(= round-index (:round-index %))
+ seeded-attempt-events)))
+ (range 4))))
+ (assert (every? #(= :stratified-beta-binomial-v1 (:algorithm-id %))
+ seeded-attempt-events))
+ (assert (= worked-strata
+ (attempt-events->strata (:raw-responses worked-fixture))))
(assert (= {:alpha 5.0 :beta 1.0} (posterior-parameters 4 4)))
(assert (= 0 (collapse-response :wrong)
(collapse-response :dont-know)))
(assert (not= :wrong :dont-know))
(assert (= 4334 worked-analytic-mean))
+ (assert (= {:complete-round? true
+ :assess? true
+ :half-width 922.5
+ :target? false
+ :soft-max? false
+ :stop? false}
+ (stopping-check {:items-tested 32
+ :interval [3404 5249]
+ :pool-size 8000})))
(assert (every? #(not (:assess? (stopping-check
{:items-tested %
:interval [0 8000]
diff --git a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive.cljs b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive.cljs
index 963484f8..27d63869 100644
--- a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive.cljs
+++ b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive.cljs
@@ -147,20 +147,37 @@
:color "var(--bs-body-color, #212529)"}}
label])
-(def balanced-response-sequence
- [:correct :wrong :dont-know :correct :correct :dont-know :wrong :correct
- :dont-know :correct :wrong :wrong :correct :dont-know :correct :correct])
-
-(def balanced-selection-limit 16)
+(def seeded-attempt-responses-by-stratum
+ [[:correct :correct :correct :correct]
+ [:correct :correct :correct :correct]
+ [:correct :correct :correct :wrong]
+ [:correct :correct :correct :dont-know]
+ [:correct :correct :wrong :dont-know]
+ [:correct :wrong :dont-know :wrong]
+ [:correct :dont-know :wrong :dont-know]
+ [:wrong :dont-know :wrong :dont-know]])
+
+(def seeded-stratum-orders
+ [[3 8 1 6 4 2 7 5]
+ [5 2 7 1 8 4 3 6]
+ [8 4 6 2 7 3 5 1]
+ [2 6 1 7 3 5 8 4]])
+
+(def seeded-attempt-selection-limit 32)
+(def balanced-selection-limit seeded-attempt-selection-limit)
(defonce balanced-state (r/atom {:revealed 0}))
(defn scheduled-selection [index]
- (let [stratum (inc (mod index 8))
- round (inc (quot index 8))]
+ (let [presentation-index (mod index 8)
+ round-index (quot index 8)
+ stratum (get-in seeded-stratum-orders
+ [round-index presentation-index])
+ round (inc round-index)]
{:stratum stratum
:round round
:item-id (str "S" stratum "-R" round)
- :response (nth balanced-response-sequence index)}))
+ :response (get-in seeded-attempt-responses-by-stratum
+ [(dec stratum) (dec round)])}))
(defn reveal-next-selection! []
(swap! balanced-state update :revealed
@@ -173,11 +190,11 @@
(let [revealed (:revealed @balanced-state)
selections (mapv scheduled-selection (range revealed))
latest (peek selections)
- complete? (= revealed balanced-selection-limit)]
+ complete? (= revealed seeded-attempt-selection-limit)]
[:section.ve-round-shell {:aria-labelledby "balanced-round-heading"}
- [:h3#balanced-round-heading "Balanced non-adaptive rounds"]
+ [:h3#balanced-round-heading "Replay the seeded non-adaptive schedule"]
[:p
- "Two fixed demonstration rounds are queued. Each click reveals the next scheduled item and its sample response."]
+ "Four fixed rounds are queued for the synthetic attempt. Each click reveals the next scheduled item and its preserved response."]
[:div.ve-round-grid
(for [stratum (range 1 9)
:let [seen (filterv #(= stratum (:stratum %)) selections)
@@ -190,16 +207,16 @@
(str "Latest: " (:item-id most-recent)
" · " (response-label (:response most-recent)))
"Latest: none")]
- [:small (if (< next-round 3)
+ [:small (if (< next-round 5)
(str "Next queued: S" stratum "-R" next-round)
- "Two demonstration items used")]])]
+ "Four attempt items used")]])]
[:progress.ve-round-progress
- {:value revealed :max balanced-selection-limit
+ {:value revealed :max seeded-attempt-selection-limit
:aria-label "Scheduled demonstration items revealed"}]
[:p.ve-round-status
{:aria-live "polite"}
(cond
- complete? "Two complete rounds: every stratum supplied two unseen items."
+ complete? "Four complete rounds: every stratum supplied four unseen items; the correct counts are 4, 4, 3, 3, 2, 1, 1, 0."
latest (str "Revealed " (:item-id latest) " as "
(response-label (:response latest))
". The response did not alter any next-item queue.")
@@ -207,12 +224,12 @@
[:div.ve-button-row
[:button.ve-sampling-button.ve-primary
{:type "button" :on-click reveal-next-selection! :disabled complete?}
- (if complete? "Two rounds complete" "Reveal next scheduled item")]
+ (if complete? "Four rounds complete" "Reveal next scheduled item")]
[:button.ve-sampling-button
{:type "button" :on-click reset-balanced-rounds!}
"Reset"]]
[:p.ve-sample-note
- "The response labels are illustrative. Schedule order is S1 through S8 in every round, independent of all answers."]]))
+ "This is the article's seeded synthetic attempt. Each stored round has one item per stratum in a seeded shuffled presentation order fixed independently of all answers."]]))
(defn make-rng [seed]
#js {:state (mod seed 2147483647)})
@@ -488,7 +505,7 @@
[:circle {:class (if (= index (dec draw-count)) "ve-dot-latest" "ve-dot")
:cx x :cy y :r (if (= index (dec draw-count)) 3.8 2.4)}])
[:text {:x 360 :y 281 :text-anchor "middle" :font-size 13 :fill "currentColor"}
- "Total known pairs in the fixed 8,000-pair pool"]]
+ "Receptively known lemma–surface-form pairs in the fixed 8,000-pair pool"]]
[:figcaption.ve-sample-note
"One dot is one complete eight-stratum draw. Blue line: live central 95% interval. Red marker: live sample mean. Both stabilize as dots accumulate."]]
[:div.ve-empty-sample
@@ -552,8 +569,8 @@
(def initial-stopping-state
(merge authoritative-stopping-settings
- {:items-tested 40
- :half-width 750}))
+ {:items-tested 32
+ :half-width 922.5}))
(defonce stopping-state (r/atom initial-stopping-state))
@@ -606,15 +623,15 @@
soft-max? "Recommend stop: the soft cap is reached."
:else "Continue: assessed, but neither stopping condition is met.")]
[:section.ve-stop-shell {:aria-labelledby "stopping-explorer-heading"}
- [:h3#stopping-explorer-heading "Teaching-only stopping-rule explorer"]
+ [:h3#stopping-explorer-heading "Continue the seeded attempt or stop?"]
[:div {:class (str "ve-stop-banner "
(if defaults? "is-default" "is-counterfactual"))}
[:strong (if defaults?
- "Authoritative v1 defaults"
+ "Authoritative Proposal 1 defaults"
"Counterfactual teaching settings")]
[:span (if defaults?
" Minimum 32 · target 10% · soft cap 96."
- " These controls do not rewrite the article's v1 rule.")]]
+ " These controls do not rewrite Proposal 1's versioned rule.")]]
[:div.ve-stop-grid
[stopping-range {:id "stop-minimum" :label "Minimum items"
:value minimum :min 8 :max 64 :step 8 :suffix ""
@@ -629,7 +646,7 @@
:value items-tested :min 8 :max 160 :step 4 :suffix ""
:on-change #(update-stopping-setting! :items-tested %)}]
[stopping-range {:id "stop-width" :label "Observed interval half-width"
- :value half-width :min 200 :max 2000 :step 50 :suffix " pairs"
+ :value half-width :min 200 :max 2000 :step 0.5 :suffix " pairs"
:on-change #(update-stopping-setting! :half-width %)}]]
[:div.ve-stop-result {:aria-live "polite"}
[:strong decision]
@@ -644,7 +661,7 @@
[:div.ve-button-row
[:button.ve-sampling-button
{:type "button" :on-click reset-stopping-settings!}
- "Reset v1 defaults"]]
+ "Reset Proposal 1 defaults"]]
[:p.ve-sample-note
"Voluntary stopping remains available in every scenario and is intentionally outside this statistical recommendation."]]))
@@ -658,7 +675,7 @@
:border-radius ".5rem"
:padding "clamp(.75rem, 3vw, 1.25rem)"
:min-width 0}}
- [:h3#simulator-heading {:style {:margin-top 0}} "One-stratum update simulator"]
+ [:h3#simulator-heading {:style {:margin-top 0}} "Replay one attempt stratum"]
[:p {:aria-live "polite"}
[:strong (str "Observed: " k " correct of " n ". ")]
(str "Current posterior Beta(" alpha "," beta "); mean knowing rate "
@@ -678,7 +695,7 @@
:color "var(--bs-body-color, #212529)"
:opacity 0.75
:margin-bottom 0}}
- "Keyboard: Tab to a button, then press Enter or Space. Wrong and “don't know” contribute the same stage-one likelihood, while their raw values remain distinct."]]))
+ "Keyboard: Tab to a button, then press Enter or Space. Wrong and “don't know” contribute the same Proposal 1 likelihood, while their raw values remain distinct."]]))
(defn ^:export mount []
(when-let [root (js/document.getElementById "balanced-round-simulator")]
@@ -690,6 +707,7 @@
(when-let [root (js/document.getElementById "stopping-rule-explorer")]
(rdom/render [stopping-rule-explorer] root)))
-(if (= "loading" js/document.readyState)
- (.addEventListener js/document "DOMContentLoaded" mount)
- (mount))
+(when (exists? js/document)
+ (if (= "loading" js/document.readyState)
+ (.addEventListener js/document "DOMContentLoaded" mount)
+ (mount)))
diff --git a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_preview.png b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_preview.png
index 62f9c7af..fdd1b69b 100644
Binary files a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_preview.png and b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass_preview.png differ
diff --git a/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-published-scoring-example.jpg b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-published-scoring-example.jpg
new file mode 100644
index 00000000..49e9de49
Binary files /dev/null and b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-published-scoring-example.jpg differ
diff --git a/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-starting-level.jpg b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-starting-level.jpg
new file mode 100644
index 00000000..dcf7a158
Binary files /dev/null and b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-starting-level.jpg differ
diff --git a/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-unanswered-sentence-item.jpg b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-unanswered-sentence-item.jpg
new file mode 100644
index 00000000..b15338a4
Binary files /dev/null and b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-2026-07-17-unanswered-sentence-item.jpg differ
diff --git a/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-captures-2026-07-17.edn b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-captures-2026-07-17.edn
new file mode 100644
index 00000000..1a1d315d
--- /dev/null
+++ b/src/language_learning/vocabulary_estimation/lexibench_captures/lexibench-captures-2026-07-17.edn
@@ -0,0 +1,41 @@
+{:schema-version 1
+ :capture-set-id "lexibench-production-2026-07-17"
+ :captured-by "Codex internal browser (coordinator agent)"
+ :browser-surface :codex-internal-browser
+ :capture-policy :read-only
+ :theme :light
+ :notes "Dated production evidence. Replacing any image requires a new dated filename, capture record, and hash; do not overwrite this set after publication. Files preserve the JPEG bytes returned by the browser capture surface."
+ :captures
+ [{:id :starting-level
+ :file "lexibench-2026-07-17-starting-level.jpg"
+ :media-type "image/jpeg"
+ :sha-256 "4b5125da52f32e1118600a859536ef97be08585dba5aa429870c4864e51fcc59"
+ :source-url "https://lexibench.com/#/"
+ :capture-date "2026-07-17"
+ :viewport {:width 1920 :height 1080}
+ :product-status :current-production
+ :recapture-state "Open the test route in a clean desktop viewport before beginning; show the passive-vocabulary explanation, all seven starting-level choices, the 80-item duration note, and Begin Test."
+ :alt "LexiBench start screen explaining passive vocabulary and offering seven starting-level choices before an 80-item test."
+ :prose-equivalent "The start screen defines passive vocabulary, asks for a starting level from pre-A1 through C2, and says the test has 80 sentence-context items."}
+ {:id :unanswered-sentence-item
+ :file "lexibench-2026-07-17-unanswered-sentence-item.jpg"
+ :media-type "image/jpeg"
+ :sha-256 "26ccf39adf58d4c133c7df3284aefb01956c4d0d92db39a9ecc137a5025ac4b4"
+ :source-url "https://lexibench.com/#/"
+ :capture-date "2026-07-17"
+ :viewport {:width 1280 :height 1133}
+ :product-status :current-production
+ :recapture-state "Begin a production test from the default starting level, then capture the first live sentence item at 0/80 before selecting or submitting any answer; include the sentence, all choices, and don't-know action."
+ :alt "Unanswered LexiBench sentence item at zero of eighty, with one highlighted Polish form, five English choices, and a don't-know option."
+ :prose-equivalent "Before any response, the live item shows progress at 0/80, asks for the highlighted Polish form’s meaning in its sentence, offers five translations, and keeps a separate don't-know response."}
+ {:id :published-scoring-example
+ :file "lexibench-2026-07-17-published-scoring-example.jpg"
+ :media-type "image/jpeg"
+ :sha-256 "960f84398310b2eaece5f58712e593a367d1ed2a163ceab010a31999f953ea50"
+ :source-url "https://lexibench.com/#/current/scoring"
+ :capture-date "2026-07-17"
+ :viewport {:width 1920 :height 1080}
+ :product-status :current-production
+ :recapture-state "Open Current > Scoring, use the Quick view anchor for Live estimate and final result, and capture the published static live/final example including its center estimate and likely range."
+ :alt "Published LexiBench scoring example showing a live and final estimate of about 1,450 recognized Polish lemmas with a likely range of 1,050 to 1,900."
+ :prose-equivalent "The published example reports about 1,450 recognized Polish lemmas, shows 1,050–1,900 as the likely range, and labels the result approximate rather than exact."}]}
diff --git a/src/language_learning/vocabulary_estimation/managing_brilliant_but_uneven_minds.clj b/src/language_learning/vocabulary_estimation/managing_brilliant_but_uneven_minds.clj
index def772f6..ec0f2941 100644
--- a/src/language_learning/vocabulary_estimation/managing_brilliant_but_uneven_minds.clj
+++ b/src/language_learning/vocabulary_estimation/managing_brilliant_but_uneven_minds.clj
@@ -5,7 +5,7 @@
:clay {:hide-info-line true
:title "Managing Brilliant but Uneven Minds: My Theory-to-Algorithm Workflow"
:quarto {:author :jamiep
- :description "Why managing fast, uneven AI agents requires explicit assumptions, executable research, and independent validation gates."
+ :description "A personal account of managing fast, uneven AI agents through explicit decisions, executable work, independent validation, and retained human responsibility."
:type :post
:date "2026-07-15"
:image "managing_brilliant_but_uneven_minds_preview.png"
@@ -23,8 +23,8 @@
(kind/hiccup
[:style
(str
- ":root{--mw-accent:#1464b5;--mw-accent-soft:#e5f1fb;--mw-warm:#9a4b00;--mw-warm-soft:#fff0df;--mw-success:#0f695f;--mw-success-soft:#e2f4f0;--mw-muted:#4f5b66}"
- ".quarto-dark{--mw-accent:#73b7ff;--mw-accent-soft:#173653;--mw-warm:#ffc27a;--mw-warm-soft:#4a2d12;--mw-success:#64d8c7;--mw-success-soft:#163d38;--mw-muted:#b9c7d2}"
+ ":root{--mw-accent:#1464b5;--mw-warm:#9a4b00;--mw-success:#0f695f;--mw-muted:#4f5b66}"
+ ".quarto-dark{--mw-accent:#73b7ff;--mw-warm:#ffc27a;--mw-success:#64d8c7;--mw-muted:#b9c7d2}"
"#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}"
".mw-opening{border-left:5px solid var(--mw-accent);border-radius:.45rem;padding:clamp(1rem,3vw,1.4rem);margin:0 0 1.5rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 87%,var(--mw-accent) 13%);font-size:1.08rem;line-height:1.65}.mw-opening p:last-child{margin-bottom:0}"
".mw-grid,.mw-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.85rem;margin:1.25rem 0}"
@@ -34,76 +34,52 @@
".mw-table-wrap{max-width:100%;overflow-x:auto;margin:1.25rem 0}.mw-table{width:100%;border-collapse:collapse}.mw-table th,.mw-table td{padding:.65rem .75rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:left;vertical-align:top;min-width:8rem}.mw-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}.mw-table code{overflow-wrap:anywhere;white-space:normal}"
".mw-code{max-width:100%;overflow-x:auto;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.75rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 94%,var(--mw-accent) 6%)}"
".mw-cycle-caption,.mw-note{font-size:.9rem;color:var(--mw-muted)}.mw-cycle-caption{text-align:center}"
- ".series-toc{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.6rem;padding:clamp(.85rem,3vw,1.2rem);margin:0 0 1.4rem;background:var(--bs-body-bg,#fff)}.series-toc h2{font-size:1.2rem;margin:0 0 .55rem}.series-toc p{margin:0 0 .7rem}.series-toc ol{margin:0;padding-left:1.65rem}.series-toc li{padding:.18rem .45rem}.series-status{display:inline-block;margin-left:.35rem;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--mw-muted)}.series-current{margin:.35rem 0 .35rem -.7rem;border-left:4px solid var(--mw-accent);border-radius:.4rem;padding:.6rem .75rem!important;background:color-mix(in srgb,var(--bs-body-bg,#fff) 84%,var(--mw-accent) 16%);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--mw-accent) 35%,transparent);font-weight:700}.series-current>a{color:var(--mw-accent)}.series-current .series-status{border-radius:999px;padding:.18rem .48rem;background:var(--mw-accent);color:#fff}.quarto-dark .series-current .series-status{color:#10212b}"
"@media(max-width:575px){.mw-table th,.mw-table td{padding:.5rem}.mw-card{padding:.8rem}}")])
^:kindly/hide-code
-(kind/hiccup
- [:nav.series-toc {:aria-labelledby "series-contents-heading"}
- [:h2#series-contents-heading "Theory to vocabulary-estimation series"]
- [:p "Learning the theory, making the assumptions explicit, and developing a scorer for "
- [:a {:href "https://lexibench.com/"} "Lexibench.com"] "."]
- [:ol {:start 0}
- [:li.series-current
- [:a {:href "managing_brilliant_but_uneven_minds.html"
- :aria-current "page"}
- "Managing brilliant but uneven minds: my theory-to-algorithm workflow"]
- [:span.series-status "you are here"]]
- [:li [:a {:href "bayes_theorem_simulations.html"}
- "Bayes' theorem from uncertainty to decision"]
- [:span.series-status "published"]]
- [:li [:a {:href "beta_binomial_first_pass.html"}
- "Estimating vocabulary size with a simple Bayesian model"]
- [:span.series-status "published"]]
- [:li [:a {:href "pair_frequency_logistic_v2_article.html"}
- "Does pair frequency predict learner responses?"]
- [:span.series-status "published"]]
- [:li "From Self-Reported CEFR to a Versioned Lemma–Form-Pair Pool"
- [:span.series-status "planned"]]
- [:li "From Correlated Form Pairs to Latent Lemma Knowledge"
- [:span.series-status "planned"]]
- [:li "Modelling Correct, Wrong, and Don't-Know Separately"
- [:span.series-status "planned"]]
- [:li "Calibrating Items Before IRT and Adaptive Selection"
- [:span.series-status "planned"]]
- [:li "When Contexts and Senses Become Identifiable"
- [:span.series-status "planned"]]]])
-
-^:kindly/hide-code
-(controls/install)
+(controls/install
+ {:article-id :workflow
+ :sections [{:id "management-job" :label "The management job"}
+ {:id "changed-cost-curve" :label "A changed cost curve"}
+ {:id "decisions-before-delegation" :label "Decide before delegating"}
+ {:id "theory-to-algorithm-cycle" :label "The theory-to-algorithm cycle"}
+ {:id "independent-validation" :label "Independent validation"}
+ {:id "repository-boundary" :label "Repository boundary"}
+ {:id "human-ownership" :label "What the human still owns"}
+ {:id "practical-influences" :label "Practical influences"}]
+ :technical-sections [{:id "sources" :label "Sources and further reading"}]})
^:kindly/hide-code
(kind/hiccup
[:div.mw-opening
- [:p "I am genuinely excited. I now have brilliant but profoundly uneven minds at my command: a new natural-language interface to the digital world and to a vast body of online knowledge. I can call on ten agents concurrently and watch them work at superhuman speed on some narrow tasks. The job description of a knowledge worker is changing accordingly. A central question is no longer only “How do I do this work?” but “How do I become an effective manager of these interesting minds?” Their speed, range, and stamina are extraordinary. Their judgment, context, and accountability are not. The fascinating new craft is learning how to work around those weaknesses and use those superhuman abilities to produce good code and good knowledge work—not AI slop."]])
+ [:p "I am genuinely excited. I now have brilliant but profoundly uneven minds at my command: a natural-language interface to the digital world and a vast body of online knowledge. I can call on ten agents concurrently and watch them work at superhuman speed on some narrow tasks. Their range and stamina are extraordinary. Their judgment, context, and accountability are not. My new craft is learning to manage those strengths and weaknesses so that the result is good code and good knowledge work—not AI slop."]])
^:kindly/hide-code
(kind/hiccup
[:ol.article-chapter-map
- [:li [:strong "Manage"] [:br] "Treat agent output as delegated work, not revealed truth."]
- [:li [:strong "Recognise disruption"] [:br] "Ask what a new cost and capability curve changes."]
- [:li [:strong "Define"] [:br] "Turn users' tacit expectations into an explicit estimand."]
- [:li [:strong "Ground"] [:br] "Separate theory already applied from ideas still to test."]
- [:li [:strong "Cycle"] [:br] "Version assumptions, evidence, candidates, and decisions."]
- [:li [:strong "Inspect"] [:br] "Publish the workflow and its complete case study together."]
- [:li [:strong "Learn"] [:br] "Borrow useful practices without outsourcing responsibility."]])
+ [:li [:strong "Decide"] [:br] "Name the outcome, authority, and acceptable risk."]
+ [:li [:strong "Delegate"] [:br] "Give agents bounded, inspectable work."]
+ [:li [:strong "Execute"] [:br] "Turn claims into code, examples, and evidence."]
+ [:li [:strong "Validate"] [:br] "Keep workflow, model, software, and publication checks distinct."]
+ [:li [:strong "Preserve"] [:br] "Version assumptions, failures, decisions, and rollback points."]
+ [:li [:strong "Own"] [:br] "Retain human responsibility for purpose and consequences."]])
-;; ## The management job
+;; ## The management job {#management-job}
;;
;; Calling these systems “minds” is deliberately provocative, but it is not a
;; claim that they are people. It describes the experience of delegating a
;; messy intellectual task, receiving a surprising interpretation, challenging
;; it, and watching another attempt arrive in seconds. Natural language is the
-;; interface, but the useful agents are not confined to chat. They can search,
-;; read a repository, run programs, inspect a browser, edit an article, and
-;; compare the result with an explicit test.
+;; interface, but useful agents are not confined to chat. They can search, read
+;; a repository, run programs, inspect a browser, edit an article, and compare
+;; the result with an explicit test.
;;
-;; The speed is real, but “superhuman” needs a boundary. An agent can transform
-;; text, search a codebase, or try variations far faster than I can. It does not
-;; thereby acquire a superhuman understanding of my users, my product, the
-;; consequences of a wrong decision, or the scientific status of a claim. Ten
-;; agents can also make ten fast, plausible mistakes. Parallelism multiplies a
-;; good process; it does not create one.
+;; “Superhuman” also needs a boundary. An agent can transform text, search a
+;; codebase, or try variations far faster than I can. It does not thereby gain
+;; a superhuman understanding of my users, the consequences of a wrong
+;; decision, or the scientific status of a claim. Ten agents can make ten fast,
+;; plausible mistakes. Parallelism multiplies a management system; it does not
+;; create one.
^:kindly/hide-code
(kind/hiccup
@@ -118,207 +94,103 @@
[:section.mw-card
[:h3 "What remains dangerously uneven"]
[:ul
- [:li "Confidently filling gaps with a plausible invention."]
+ [:li "Filling a gap with a confident, plausible invention."]
[:li "Optimising the task it heard instead of the outcome I meant."]
- [:li "Losing context or treating a recent source as established authority."]
- [:li "Announcing completion before the whole result has been checked."]]]
+ [:li "Treating a recent source as established authority."]
+ [:li "Announcing completion before the whole result is checked."]]]
[:section.mw-card
[:h3 "What the human manager must own"]
[:ul
- [:li "The purpose, users, definitions, priorities, and acceptable risk."]
+ [:li "Purpose, users, definitions, priorities, and acceptable risk."]
[:li "Which sources and evidence deserve authority."]
[:li "Permission boundaries and irreversible decisions."]
- [:li "Independent review and the final claim that the work is good enough."]]]])
+ [:li "Independent review and the final claim that work is good enough."]]]])
-;; The “junior developer” analogy is useful if it changes behaviour. I do not
-;; hand a junior colleague one vague sentence, merge whatever appears, and then
-;; blame the colleague. I explain the outcome, let questions reveal ambiguity,
-;; divide work into inspectable slices, provide tools, and require tests and
-;; review. Agents need an even more explicit version of that management because
-;; they are extraordinarily productive, do not accumulate responsibility, and
-;; may repeat the same kind of mistake in a fresh, fluent form.
+;; The “junior developer” analogy is useful only if it changes my behaviour. I
+;; do not hand a junior colleague one vague sentence, merge whatever appears,
+;; and blame the colleague. I explain the outcome, let questions expose
+;; ambiguity, divide work into inspectable slices, provide tools, and require
+;; tests and review. Agents need an even more explicit version of that
+;; management because they are extraordinarily productive, do not accumulate
+;; responsibility, and can repeat the same mistake in a fresh, fluent form.
;;
-;; This is why established engineering practices matter more, not less. Clear
-;; requirements, version control, small changes, executable examples, test-first
-;; regression work, peer review, least privilege, and rollback are mechanisms
-;; for turning fallible effort into dependable output. An LLM can help perform
-;; nearly every step. It cannot be the reason that a step is trusted.
+;; Established engineering practices therefore matter more, not less. Clear
+;; requirements, version control, small changes, executable examples,
+;; test-first regression work, peer review, least privilege, and rollback turn
+;; fallible effort into dependable output. An LLM can help perform nearly every
+;; step. It cannot be the reason that a step is trusted.
;;
-;; ## A book for this moment: *The Innovator's Dilemma*
+;; ## A changed cost curve {#changed-cost-curve}
;;
;; I have not yet read Clayton M. Christensen's
-;; [*The Innovator's Dilemma*](https://store.hbr.org/product/the-innovator-s-dilemma-with-a-new-foreword-when-new-technologies-cause-great-firms-to-fail/10706),
-;; and I am excited to. Its central idea seems exceptionally relevant to the
-;; present moment. Successful organisations can make sensible decisions—listen
-;; to their best customers, improve the products those customers value, demand
-;; attractive margins—and still miss a disruptive change. The early disruptive
-;; offering is often worse on the dimensions the established market already
-;; rewards. It may instead be cheaper, simpler, more convenient, or useful to a
-;; new or overlooked group. As it improves, it can redraw the market and its
-;; value network before the incumbent's normal decision machinery knows how to
-;; take it seriously.
-;;
-;; That is more precise than using *disruptive* as a synonym for impressive new
-;; technology. Christensen's dilemma is that the incumbent's apparently good
-;; management can rationally reject the small, low-margin, initially inferior
-;; opportunity. The structures that made the organisation excellent at the old
-;; game can prevent it from learning the new one.
-
-^:kindly/hide-code
-(kind/hiccup
- [:div.mw-callout.success
- [:strong "Popular and authoritative—but not scripture"]
- [:p "The book deserves to be called a management classic. Harvard Business Review Press describes it as a Wall Street Journal and Businessweek bestseller and one of the Economist's six most important business books. Harvard Business School records that it was also a New York Times bestseller and won the 1997 Global Business Book Award; Christensen was a Harvard Business School professor whose work put “disruptive innovation” into the managerial vocabulary."]
- [:p "That establishes extraordinary influence and practical authority. It does not make every case or extrapolation settled science. Later research has refined, disputed, and frequently complained about misuse of the theory. That distinction is itself relevant to this series: a powerful framework should generate sharper questions and testable predictions, not become a label that excuses us from evidence."]])
-
-;; Applying the book to today's knowledge-worker disruption is my inference,
-;; not a claim that Christensen predicted coding agents. AI systems are still
-;; worse than skilled humans on many dimensions by which professional work has
-;; traditionally been judged. At the same time, they radically change the cost,
-;; speed, availability, and accessibility of producing software, analysis,
-;; writing, designs, and operational work. A person who demands that an agent
-;; first reproduce an expert's entire old job, unaided and without error, may
-;; miss the new workflow forming around the technology.
-;;
-;; The useful question is not simply “Will AI replace white-collar jobs?” It is:
-;; **which units of work, value networks, and organisational habits change when
-;; competent intellectual labour becomes abundant, fast, conversational, and
-;; uneven?** Established organisations are optimised around scarce human
-;; attention, meetings, hand-offs, and implementation time. Agent-native work
-;; can make specification, judgment, validation, and responsibility the new
-;; bottlenecks. Knowledge workers are both the managers of this disruptive
-;; capability and part of the sector being disrupted by it.
-;;
-;; ## From an expectation to an estimand
-;;
-;; My concrete case is a vocabulary-size scorer for
-;; [Lexibench](https://lexibench.com/). I want to encode an algorithm that does
-;; what learners reasonably expect, in a rigorous and inspectable way. That
-;; sentence hides the hardest work. Users do not arrive with one formal,
-;; unanimous definition. I have to turn likely expectations into hypotheses,
-;; state them, show learners what the result means, check whether they interpret
-;; it as intended, and version the model if the target changes.
-;;
-;; A useful technical word here is **estimand**: the quantity an algorithm
-;; claims to estimate. “How many words do you know?” is not yet an estimand. It
-;; does not say what counts as a word, which meanings count, what population of
-;; vocabulary provides the denominator, or what evidence counts as knowledge.
-;; An authoritative answer cannot be more precise than its question.
-;;
-;; Consider the dictionary form, or **lemma**, of a Polish word. Does knowing
-;; the lemma mean knowing every possible inflected surface form? Or does it mean
-;; recognising the intended meaning across the surface forms that account for,
-;; say, 70% of the lemma's observed occurrences? The 70% is deliberately an
-;; example, not a threshold smuggled into the current scorer. It exposes a real
-;; choice between exhaustive type coverage and frequency-weighted coverage of
-;; language people are likely to encounter.
-;;
-;; Polish makes this choice impossible to ignore. Nouns, adjectives, verbs,
-;; numerals, and other word classes change form to express grammatical
-;; information. The
-;; [Grammatical Dictionary of Polish](https://sgjp.pl/about/) describes the
-;; inflection of hundreds of thousands of lexemes through millions of textual
-;; word forms. One lemma can therefore connect to many surface forms, sometimes
-;; with the same spelling serving several grammatical functions. “One word” is
-;; not a harmless database row.
-
-^:kindly/hide-code
-(kind/hiccup
- [:div.mw-table-wrap
- [:table.mw-table
- [:caption "Hidden choices behind a vocabulary-size result"]
- [:thead
- [:tr [:th {:scope "col"} "Question"]
- [:th {:scope "col"} "A tempting shortcut"]
- [:th {:scope "col"} "What must become explicit"]]]
- [:tbody
- [:tr
- [:th {:scope "row"} "What is counted?"]
- [:td "Words"]
- [:td "Lemmas, surface forms, lemma–form pairs, meanings, or versioned test items."]]
- [:tr
- [:th {:scope "row"} "What does “know” mean?"]
- [:td "Answered correctly once"]
- [:td "Passive recognition, production, repeated recognition across forms, or a latent probability."]]
- [:tr
- [:th {:scope "row"} "What does a response prove?"]
- [:td "Correct equals known"]
- [:td "The roles of guessing, slips, context, distractors, ambiguity, and item quality."]]
- [:tr
- [:th {:scope "row"} "What is the total?"]
- [:td "The language"]
- [:td "A fixed, versioned pool with declared inclusion and exclusion rules."]]
- [:tr
- [:th {:scope "row"} "When can the test stop?"]
- [:td "After a convenient number"]
- [:td "A specified uncertainty target, minimum evidence, and maximum burden."]]]]])
-
-;; The current first-pass estimand is deliberately narrower:
+;; [*The Innovator's Dilemma*](https://store.hbr.org/product/the-innovator-s-dilemma-with-a-new-foreword-when-new-technologies-cause-great-firms-to-fail/10706).
+;; What follows is a question prompted by publisher summaries, Christensen's
+;; historical influence, and later scholarship—not my account of an unread
+;; book. The idea I find useful is that sensible organisations can optimise for
+;; the qualities their established customers value and still miss an initially
+;; weaker offering built around a different cost, convenience, or value curve.
;;
-;; > **Receptive knowledge of lemma–surface-form pairs in a fixed, versioned
-;; > pool.**
+;; Applying that idea to agents is my inference, not a claim that Christensen
+;; predicted coding agents. These systems remain worse than skilled humans on
+;; many traditional measures of professional work, while radically changing
+;; the speed, price, accessibility, and availability of intellectual labour.
+;; The question is not whether an agent can reproduce an expert's entire old
+;; job unaided. It is which units of work and organisational habits change when
+;; competent output becomes abundant, conversational, and uneven—and when
+;; specification, judgment, validation, and responsibility become the scarce
+;; resources.
;;
-;; This is passive receptive vocabulary: whether a learner recognises an
-;; intended meaning, not whether they can produce the form in speech or writing.
-;; Context and answer choices help operationalise one intended meaning, but the
-;; current frequency data does not identify senses and the scorer does not
-;; pretend to estimate sense-specific knowledge. Nor does pair knowledge yet
-;; aggregate into a principled latent claim that the learner “knows the lemma.”
+;; The book's influence is not in doubt: Harvard Business Review Press records
+;; its bestseller reception, and Harvard Business School records its 1997
+;; Global Business Book Award and Christensen's role in putting disruptive
+;; innovation into managerial vocabulary. Influence is not settled science.
+;; Later research has refined the theory, disputed parts of it, and documented
+;; its misuse. I want a framework that sharpens questions I can test, not a
+;; prestigious label that excuses me from evidence.
;;
-;; Those are not embarrassing omissions to conceal. They are assumptions and
-;; open questions to publish. Learners should see qualified results that say
-;; what was estimated, from which pool, by which algorithm version, with what
-;; uncertainty. User research then checks whether those words create the
-;; understanding I intended. Transparency is necessary; comprehension is the
-;; test of whether the transparency worked.
+;; ## Decide before delegating {#decisions-before-delegation}
;;
-;; ## Theory already used, and theory still to earn
+;; The expensive part of agent management is often deciding what the work
+;; should mean. Before implementation I must name the user outcome, translate
+;; an informal expectation into an explicit target, choose which sources have
+;; authority, state exclusions, and decide what evidence could prove the idea
+;; inadequate. If those decisions stay tacit, an agent will make them for me.
+;; Its answer may be elegant and internally consistent while solving the wrong
+;; problem.
;;
-;; LLMs are fabulous research tools for this work. They can map unfamiliar
-;; terminology, propose search strategies, compare papers, challenge an
-;; assumption, explain a derivation several ways, and help translate a model
-;; contract into executable code. They are discovery and synthesis engines,
-;; not scientific authorities. A fluent summary sends me to the primary source;
-;; it does not replace it.
+;; My LexiBench vocabulary-estimation work is the concrete case study. The
+;; product purpose, learner-facing quantity, complete measurement chain, current
+;; research targets, and deferred questions now live in
+;; [Article 2, “Why estimate vocabulary?”](why_estimate_vocabulary.html).
+;; Article 1 owns how I organise the work; Article 2 owns why the product exists
+;; and what it is trying to measure.
^:kindly/hide-code
(kind/hiccup
- [:div.mw-grid
- [:section.mw-card
- [:h3 "Applied in the published work"]
- [:ul
- [:li "Measurement discipline: name the estimand, population, conditions, and exclusions."]
- [:li "Bayesian inference: update uncertainty from responses and predict untested pairs."]
- [:li "Stratified sampling: balance observations across a provisional frequency proxy."]
- [:li "Sequential stopping: stop at round boundaries when a declared interval target or soft maximum is reached."]
- [:li "Precommitted simulation validation: freeze scenarios, metrics, thresholds, and seeds before held-out evidence."]]]
- [:section.mw-card
- [:h3 "Important, but not yet claimed"]
- [:ul
- [:li "A versioned conversion from CEFR and corpus evidence into a learner-relevant pool."]
- [:li "Hierarchical models connecting correlated surface-form evidence to latent lemma knowledge."]
- [:li "Separate guessing, slipping, wrong, and don't-know processes."]
- [:li "Calibrated item difficulty, item-response theory, and adaptive selection."]
- [:li "Multiple contexts or senses only when stable identifiers and repeated evidence make them identifiable."]]]])
+ [:div.mw-callout.warm
+ [:strong "Delegation starts after the important nouns are defined"]
+ [:p "A fluent implementation can hide disagreement about the user, target quantity, population, evidence, or acceptable error. I write those decisions down, give them versions, and make later changes visible."]])
-;; The practical goal is to reach useful accuracy while testing as few items as
-;; possible. That is not permission to claim the present scorer is optimal.
-;; The current selector is balanced and non-adaptive; frequency is a proxy, not
-;; calibrated item difficulty. Calibration, IRT, and adaptive testing belong to
-;; later candidates, each with evidence that can fail.
+;; LLMs are fabulous research tools here. They can map unfamiliar terminology,
+;; propose searches, compare papers, challenge an assumption, explain a
+;; derivation several ways, and translate a model contract into code. They are
+;; discovery and synthesis engines, not scientific authorities. A fluent
+;; summary sends me to the primary source; it does not replace it.
;;
-;; ## The research cycle, in public
+;; ## The theory-to-algorithm cycle {#theory-to-algorithm-cycle}
;;
;; The unit of progress is not “ask an AI for the answer.” It is one versioned,
;; explainable refinement. I begin with the smallest defensible model, then
-;; change one important assumption at a time. The old model remains runnable.
-;; A failed candidate remains evidence rather than disappearing into a rewritten
-;; story.
+;; change one important assumption at a time. Theory becomes an explicit model
+;; contract; the contract becomes executable code; code produces replayable
+;; evidence; evidence decides whether a candidate earns promotion. The old
+;; model remains runnable. A failed candidate remains evidence instead of
+;; disappearing into a rewritten success story.
^:kindly/hide-code
(kind/mermaid
"flowchart LR
- U[User expectation
as a hypothesis] --> E[Explicit estimand
and exclusions]
+ U[User expectation
as a hypothesis] --> E[Explicit target
and exclusions]
E --> T[Relevant theory
and assumptions]
T --> B[Executable
baseline]
B --> C[One versioned
candidate]
@@ -328,162 +200,168 @@
D -->|no| N[Do not promote;
preserve evidence]
P --> R[Publish reasoning,
code, evidence, decision]
N --> R
- R --> L[Learner interpretation
and feedback]
+ R --> L[User interpretation
and feedback]
L --> U")
^:kindly/hide-code
(kind/hiccup
[:p.mw-cycle-caption
- "Agents can contribute throughout the loop. Human responsibility for the target, authority, gate, and decision does not move with the work."])
+ "Agents can contribute throughout the cycle. Human responsibility for the target, authority, gate, and decision does not move with the work."])
-;; Three gates answer different questions:
+;; Agents can work on separate, bounded slices, but a coordinator must preserve
+;; dependencies and integrate in order. A protocol should exist before the
+;; result it will judge. Generated evidence should identify its inputs,
+;; algorithm, seed, runtime, and output. A surprising result should send the
+;; work back around the cycle, not trigger quiet threshold changes.
+;;
+;; ## Independent validation {#independent-validation}
+;;
+;; “Validated” is too vague to be useful. I keep four questions separate:
^:kindly/hide-code
(kind/hiccup
[:dl.mw-definition-grid
+ [:div.mw-definition
+ [:dt "Workflow validation"]
+ [:dd "Do repository roles, paths, commands, ownership, and publication order resolve from the versioned profile?"]]
[:div.mw-definition
[:dt "Model validation"]
- [:dd "Does the candidate earn promotion for the declared estimand under the frozen scenarios and thresholds?"]]
+ [:dd "Does the candidate earn promotion for the declared target under frozen scenarios and thresholds?"]]
[:div.mw-definition
[:dt "Software validation"]
- [:dd "Does the implementation behave as specified, replay deterministically, and agree across its supported runtimes?"]]
+ [:dd "Does the implementation behave as specified, replay deterministically, and agree across supported runtimes?"]]
[:div.mw-definition
[:dt "Publication validation"]
- [:dd "Can a reader inspect the claim and evidence through working pages, controls, links, labels, layouts, and themes?"]]])
+ [:dd "Can a reader inspect the claim through working pages, controls, links, labels, layouts, and themes?"]]])
-;; This separation has already mattered. The first scorer estimates an
-;; independent knowing rate in each of eight frequency strata. A second
-;; candidate replaced those steps with a continuous pair-frequency curve. The
-;; candidate improved aggregate coverage and mean absolute error in its related
-;; simulations. It still failed precommitted worst-cell coverage, worst-cell
-;; error, and test-length checks. The code worked. The article rendered. The
-;; model did not earn promotion, so the first version remained the target.
+;; Passing one lane never substitutes for another. This distinction has already
+;; mattered in the case study. The first research scorer estimates independent
+;; knowing rates across eight frequency strata. A second candidate replaced
+;; those steps with a continuous pair-frequency curve. It improved aggregate
+;; coverage and mean absolute error in its held-out simulations, yet failed
+;; precommitted worst-cell coverage, worst-cell error, and test-length checks.
+;; The code worked. The article rendered. The model did not earn promotion, so
+;; the first version remained the research target.
;;
;; That negative result is the workflow doing its job. Without a frozen gate,
-;; a very productive agent—or an enthusiastic human—could select the attractive
+;; a highly productive agent—or an enthusiastic human—could select attractive
;; aggregate numbers, retune after seeing the result, and publish a polished
;; success story. The antidote to AI slop is not merely better prose. It is an
;; evidence structure that makes inconvenient results hard to erase.
;;
-;; ## What is in the public repository?
+;; ## Repository boundary {#repository-boundary}
;;
-;; I have published the complete worked example as the
+;; I have published the complete worked example in the
;; [theory-to-algorithm workflow repository](https://github.com/jamiepratt/theory-to-algorithm-workflow).
-;; It is both a reusable method and one inspectable vocabulary-estimation case
-;; study. Clojure, Clay, Quarto, Civitas, and Git submodules are the technologies
-;; used in this example; none is a requirement of the method.
+;; Its identity is deliberately dual: a reusable method plus one complete,
+;; inspectable vocabulary-estimation case study. The repository boundary makes
+;; authority explicit rather than allowing two histories to claim the same
+;; artifacts.
^:kindly/hide-code
(kind/hiccup
[:div.mw-table-wrap
[:table.mw-table
- [:caption "Repository map"]
+ [:caption "Ownership and authority in the public example"]
[:thead
- [:tr [:th {:scope "col"} "Area"]
- [:th {:scope "col"} "What it contains"]
- [:th {:scope "col"} "Why it exists"]]]
- [:tbody
- [:tr
- [:th {:scope "row"} [:code "README.md"] " and " [:code "docs/workflow/"]]
- [:td "The lifecycle, validation lanes, limits, and adaptation guide."]
- [:td "A human-readable method that can be copied to another theory-to-algorithm problem."]]
[:tr
- [:th {:scope "row"} [:code "CONTEXT.md"] " and " [:code "CONTEXT-MAP.md"]]
- [:td "Stable vocabulary, invariants, authority order, and routes to deeper context."]
- [:td "Give each fresh agent the small amount of context it must not invent."]]
- [:tr
- [:th {:scope "row"} [:code ".agents/research-workflow.json"]]
- [:td "Repository roles, article mapping, commands, commit order, and browser checks."]
- [:td "A versioned profile keeps reusable tools free of workstation-specific assumptions."]]
+ [:th {:scope "col"} "Area"]
+ [:th {:scope "col"} "Owns"]
+ [:th {:scope "col"} "Management purpose"]]]
+ [:tbody
[:tr
- [:th {:scope "row"} [:code ".agents/skills/"] " and GitHub templates"]
- [:td "Repeatable preview, publication, review, model-experiment, and pull-request interfaces."]
- [:td "Make important process steps executable and reviewable instead of relying on memory."]]
+ [:th {:scope "row"} "Workflow repository"]
+ [:td "Method, decisions, model contract, versioned profile, and reusable skills."]
+ [:td "Tell a fresh agent what is authoritative and how this project is organised."]]
[:tr
- [:th {:scope "row"} [:code "docs/language-learning/"]]
- [:td "The current scoring contract, status, decisions, and links through the evidence chain."]
- [:td "State exactly what an implementation target claims and does not claim."]]
+ [:th {:scope "row"} "Publication submodule"]
+ [:td "Executable articles, model code, fixtures, evidence, tests, and publication inputs."]
+ [:td "Pin the exact source and evidence described by the parent workflow history."]]
[:tr
- [:th {:scope "row"} [:code "clojurecivitas.github.io/"]]
- [:td "A pinned submodule containing executable articles, model code, fixtures, immutable evidence, tests, and rendering configuration."]
- [:td "Connect the public explanation to the exact publication history and evidence it describes."]]]]])
+ [:th {:scope "row"} "GitHub issues and pull requests"]
+ [:td "Future work, dependency order, review, and publication checkpoints."]
+ [:td "Keep unfinished intentions out of docs that claim to describe shipped reality."]]]]])
-;; A new project can replace the case-study material, edit the profile, and
-;; retain the lifecycle and validation separation. A curious reader can instead
-;; follow the vocabulary example end to end: the current contract, the first
-;; executable model, the preserved failed candidate, its deterministic evidence,
-;; and the tests that protect replay.
+;; The publication repository must be committed and published before the parent
+;; repository records its submodule pointer. That order is mundane but
+;; important: a reader should never receive a pointer to work that does not
+;; exist. Clojure, Clay, Quarto, Civitas, and Git submodules implement this case
+;; study; none is a requirement of the reusable method.
^:kindly/hide-code
(math/code-detail
"code-clone-workflow"
"Cloning and validating the complete workflow"
[:div
- [:p "Clone the workflow with its publication submodule, then validate the versioned repository profile before using its commands."]
+ [:p "Clone both histories, then validate the versioned repository profile before using its commands."]
[:pre.mw-code
[:code "git clone --recurse-submodules https://github.com/jamiepratt/theory-to-algorithm-workflow.git\ncd theory-to-algorithm-workflow\npython3 .agents/scripts/research_workflow.py validate"]]])
-;; ## Two practical influences
+;; ## What the human still owns {#human-ownership}
+;;
+;; My agents can propose requirements, draft a protocol, implement code, run a
+;; simulation, inspect a page, and criticise one another. I still own the reason
+;; for doing the work, the people affected, the definitions that shape the
+;; answer, the authority assigned to sources, the permissions granted to tools,
+;; and the decision to publish or deploy. I also own the possibility that my
+;; framing is wrong.
+;;
+;; Retained responsibility does not mean manually repeating every agent action.
+;; It means arranging the work so that consequential decisions are explicit and
+;; evidence reaches me in a reviewable form. I use isolated workspaces, narrow
+;; scopes, tests that observe public behaviour, immutable evidence, visual
+;; review, staged publication, and rollback. I ask another agent to review when
+;; independence helps, but I do not confuse agent disagreement or agreement
+;; with human judgment.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.mw-callout.success
+ [:strong "My acceptance test"]
+ [:p "The standard is not whether an agent produced something astonishingly quickly. It is whether humans and agents together produced something worth trusting—and whether I can explain why."]])
+
+;; ## Two practical influences {#practical-influences}
;;
-;; [Matt Pocock](https://www.mattpocock.com/) has published an excellent
-;; [full workflow walkthrough](https://www.youtube.com/watch?v=-QFHIoCo-Ko)
-;; alongside his broader [YouTube channel](https://www.youtube.com/c/mattpocockuk).
-;; I recommend his approach because it treats agent output as engineering work:
-;; interrogate a vague request before implementation, write down the destination,
-;; cut work into vertical slices that create real feedback, and use established
-;; testing and review practices. The agent is a tremendously productive junior
-;; developer, not an oracle and not the person accountable for the system.
+;; [Matt Pocock](https://www.mattpocock.com/)'s
+;; [workflow walkthrough](https://www.youtube.com/watch?v=-QFHIoCo-Ko) treats
+;; agent output as engineering work: interrogate a vague request, write down the
+;; destination, cut work into vertical slices that create feedback, and use
+;; established testing and review practices. The agent is a tremendously
+;; productive junior developer, not an oracle or the person accountable for the
+;; system.
;;
-;; [Kun Chen's](https://www.youtube.com/channel/UCb69t9ZkE5z1KvCmfJoaifA)
-;; [agentic-engineering walkthrough](https://www.youtube.com/watch?v=iQyg-KypKAA)
-;; is another important influence. His captain-and-crew framing takes the
-;; management analogy seriously: invest human attention at the beginning in
-;; deciding and explaining what should be built, delegate implementation across
+;; [Kun Chen's agentic-engineering walkthrough](https://www.youtube.com/watch?v=iQyg-KypKAA)
+;; takes the captain-and-crew analogy seriously: invest human attention at the
+;; beginning in deciding what should be built, delegate implementation across
;; isolated agents, and invest again at the end in validation. His emphasis on
-;; visual artefacts is especially useful when agents can generate information
-;; faster than a human can review walls of text.
+;; visual artefacts is valuable when agents generate information faster than a
+;; human can review walls of text.
;;
-;; I am not trying to reproduce either workflow ritual for ritual. The durable
-;; lesson is to design the organisation around the agents' actual strengths and
+;; I am not reproducing either workflow ritual for ritual. The durable lesson is
+;; to design the organisation around the agents' actual strengths and
;; weaknesses. Requirements, context, tools, isolation, feedback, validation,
;; versioned evidence, and rollback are the management system.
;;
-;; ## The work ahead
-;;
-;; We have suddenly gained access to a strange abundance of fast intellectual
-;; labour. The temptation is to ask for more output. The interesting work is to
-;; build better judgment around that output: decide what deserves to exist,
-;; clarify what it should mean, construct evidence that can contradict us, and
-;; preserve the reasoning so another person can inspect it.
-;;
-;; That is why I am doing this research cycle in public. The vocabulary scorer
-;; is useful in its own right, but it is also a demanding test of the broader
-;; workflow. It forces natural-language expectations, linguistic categories,
-;; statistical theory, code, evidence, user communication, and operational
-;; decisions into contact. Every hidden assumption that becomes visible is
-;; progress. Every candidate that fails honestly is progress too.
-;;
-;; I am excited by the minds now available to us, by Christensen's warning that
-;; good old-world management can miss a new value network, and by the new job
-;; of learning how to manage this capability well. The standard should not be
-;; whether an agent produced something astonishingly quickly. It should be
-;; whether humans and agents together produced something worth trusting.
+;; We have suddenly gained a strange abundance of fast intellectual labour. The
+;; temptation is to demand more output. The interesting work is to build better
+;; judgment around it: decide what deserves to exist, clarify what it should
+;; mean, construct evidence that can contradict us, and preserve the reasoning
+;; so another person can inspect it. Every hidden assumption that becomes
+;; visible is progress. Every candidate that fails honestly is progress too.
;;
-;; ## Sources and further reading
+;; ## Sources and further reading {#sources}
;;
;; - Clayton M. Christensen,
;; [*The Innovator's Dilemma*](https://store.hbr.org/product/the-innovator-s-dilemma-with-a-new-foreword-when-new-technologies-cause-great-firms-to-fail/10706),
-;; Harvard Business Review Press, for the book's central argument and publisher-recorded reception.
+;; Harvard Business Review Press, for the publisher's summary and reception claims.
;; - [Harvard Business School's account of Christensen's work and influence](https://www.hbs.edu/news/releases/clayton-christensen-obituary),
;; including the book's New York Times bestseller status and 1997 Global Business Book Award.
;; - Si and Chen,
;; [“A literature review of disruptive innovation: What it is, how it works and where it goes”](https://www.sciencedirect.com/science/article/pii/S0923474820300163),
;; for the theory's influence, refinements, disputes, and frequent misuse.
-;; - Woliński et al.,
-;; [“The Online Version of Grammatical Dictionary of Polish”](https://aclanthology.org/L16-1412/),
-;; and the [dictionary itself](https://sgjp.pl/about/), for explicit Polish lexeme and inflection data.
;; - [The public theory-to-algorithm workflow](https://github.com/jamiepratt/theory-to-algorithm-workflow),
-;; including its current scoring contract, workflow docs, complete case study, and adaptation guide.
-;; - [Bayes' theorem from uncertainty to decision](bayes_theorem_simulations.html),
+;; including its method, scoring contract, complete case study, and adaptation guide.
+;; - [Why estimate vocabulary?](why_estimate_vocabulary.html),
+;; [Bayes' theorem from uncertainty to decision](bayes_theorem_simulations.html),
;; [the stratified Beta–binomial first pass](beta_binomial_first_pass.html), and
;; [the continuous-frequency candidate and non-promotion decision](pair_frequency_logistic_v2_article.html).
diff --git a/src/language_learning/vocabulary_estimation/why_estimate_vocabulary.clj b/src/language_learning/vocabulary_estimation/why_estimate_vocabulary.clj
new file mode 100644
index 00000000..8b854db0
--- /dev/null
+++ b/src/language_learning/vocabulary_estimation/why_estimate_vocabulary.clj
@@ -0,0 +1,361 @@
+^{:kindly/hide-code true
+ :kindly/options
+ {:html/deps
+ [{:js ["https://cdn.jsdelivr.net/npm/scittle@0.7.23/dist/scittle.js"]}]}
+ :clay {:hide-info-line true
+ :title "Why Estimate Vocabulary?"
+ :quarto {:author :jamiep
+ :description "How a learner-facing Polish vocabulary estimate depends on explicit choices from the target pool through the final uncertainty range."
+ :type :post
+ :date "2026-07-17"
+ :image "lexibench_captures/lexibench-2026-07-17-published-scoring-example.jpg"
+ :image-alt "Published LexiBench example of an estimated recognized-Polish-lemma result with a likely range."
+ :category :concepts
+ :tags [:language-learning :vocabulary-estimation :measurement :lexibench]
+ :keywords [:receptive-vocabulary :polish-lemmas :measurement-chain :uncertainty]}}}
+
+(ns language-learning.vocabulary-estimation.why-estimate-vocabulary
+ (:require [language-learning.vocabulary-estimation.article-controls :as controls]
+ [scicloj.kindly.v4.kind :as kind]))
+
+^:kindly/hide-code
+(kind/hiccup
+ [:style
+ (str
+ ":root{--purpose-accent:#1464b5;--purpose-ink:#24313a;--purpose-soft:#e8f2fb;--purpose-warm:#9a4b00;--purpose-warm-soft:#fff0df;--purpose-green:#0f695f;--purpose-green-soft:#e2f4f0;--purpose-muted:#4f5b66}"
+ ".quarto-dark{--purpose-accent:#73b7ff;--purpose-ink:#e7eef4;--purpose-soft:#173653;--purpose-warm:#ffc27a;--purpose-warm-soft:#4a2d12;--purpose-green:#64d8c7;--purpose-green-soft:#163d38;--purpose-muted:#b9c7d2}"
+ "#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.12;overflow-wrap:anywhere}"
+ ".purpose-opening{margin:0 0 1.45rem;border-left:5px solid var(--purpose-accent);border-radius:.45rem;padding:clamp(1rem,3vw,1.4rem);background:color-mix(in srgb,var(--bs-body-bg,#fff) 87%,var(--purpose-accent) 13%);font-size:1.08rem;line-height:1.65}.purpose-opening p:last-child{margin-bottom:0}"
+ ".purpose-capture{min-width:0;margin:1.35rem 0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.65rem;padding:clamp(.6rem,2vw,1rem);background:var(--bs-body-bg,#fff)}.purpose-capture img{display:block;width:100%;height:auto;border-radius:.35rem}.purpose-capture figcaption{margin:.75rem .15rem .1rem;color:var(--purpose-muted);font-size:.9rem;line-height:1.5}"
+ ".purpose-annotation{display:block;margin-top:.35rem;color:var(--bs-body-color,#212529);font-weight:650}"
+ ".purpose-question-grid,.purpose-chain{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:.75rem;margin:1.2rem 0}"
+ ".purpose-card,.purpose-chain-step{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.55rem;padding:.85rem;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.purpose-card h3,.purpose-chain-step h3{margin:0 0 .35rem;font-size:1rem;color:var(--purpose-accent)}.purpose-card p:last-child,.purpose-chain-step p:last-child{margin-bottom:0}"
+ ".purpose-chain{counter-reset:purpose-step}.purpose-chain-step{counter-increment:purpose-step;border-top:4px solid var(--purpose-accent)}.purpose-chain-step::before{content:counter(purpose-step);display:grid;place-items:center;width:1.6rem;height:1.6rem;margin-bottom:.45rem;border-radius:50%;background:var(--purpose-accent);color:#fff;font-weight:800}"
+ ".purpose-callout{margin:1.25rem 0;border:1px solid color-mix(in srgb,var(--purpose-accent) 45%,var(--bs-border-color,#dee2e6));border-left:4px solid var(--purpose-accent);border-radius:.45rem;padding:1rem 1.1rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 91%,var(--purpose-accent) 9%)}.purpose-callout.warm{border-color:color-mix(in srgb,var(--purpose-warm) 55%,var(--bs-border-color,#dee2e6));border-left-color:var(--purpose-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 91%,var(--purpose-warm) 9%)}.purpose-callout.green{border-color:color-mix(in srgb,var(--purpose-green) 55%,var(--bs-border-color,#dee2e6));border-left-color:var(--purpose-green);background:color-mix(in srgb,var(--bs-body-bg,#fff) 91%,var(--purpose-green) 9%)}.purpose-callout strong{display:block;margin-bottom:.3rem}.purpose-callout p:last-child{margin-bottom:0}"
+ ".purpose-status-table{width:100%;border-collapse:collapse;margin:1.15rem 0}.purpose-status-table th,.purpose-status-table td{min-width:0;padding:.7rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:left;vertical-align:top;overflow-wrap:anywhere}.purpose-status-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}"
+ ".purpose-technical{margin:1.2rem 0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.55rem;background:var(--bs-body-bg,#fff)}.purpose-technical summary{padding:.75rem 1rem;color:var(--purpose-accent);font-weight:750;cursor:pointer}.purpose-technical-body{padding:0 1rem 1rem}.purpose-technical-body p:last-child{margin-bottom:0}"
+ ".purpose-source-note{font-size:.88rem;color:var(--purpose-muted)}"
+ "@media(max-width:575px){.purpose-status-table{display:block;max-width:100%;overflow-x:auto}.purpose-card,.purpose-chain-step{padding:.75rem}}")])
+
+^:kindly/hide-code
+(controls/install
+ {:article-id :purpose
+ :sections [{:id "learner-question" :label "The learner's question"}
+ {:id "what-item-hides" :label "What one item hides"}
+ {:id "pair-stepping-stone" :label "Why count pairs first"}
+ {:id "measurement-chain" :label "The complete measurement chain"}
+ {:id "three-scorers" :label "Three scorers, three statuses"}
+ {:id "learner-destination" :label "The learner-facing destination"}]
+ :technical-sections [{:id "technical-boundaries" :label "Technical boundaries"}
+ {:id "capture-provenance" :label "Capture provenance"}]})
+
+;; BEGIN ALWAYS-VISIBLE
+;; A vocabulary result looks simple: a number, perhaps a level, and a range.
+;; Yet the number answers a chain of questions that most learners should never
+;; have to formulate. What counts as one vocabulary unit? Which Polish words
+;; belong in the possible total? What does a correct multiple-choice response
+;; reveal? How should a short sample stand in for thousands of unasked items?
+;; How certain should the result appear? This series exists because every link
+;; in that chain changes what the final number means.
+;;
+;; The practical destination is easy to state. I want LexiBench to give a
+;; learner a useful estimate of **receptive Polish lemmas**: dictionary
+;; headwords they are likely to recognise when encountered in meaningful
+;; context. The route is harder. A trustworthy estimate needs explicit
+;; definitions, versioned data, inspectable items, an inference procedure, and
+;; evidence that the whole measurement behaves reasonably for different
+;; learners. The current research deliberately starts with a narrower quantity
+;; that can be defined and checked more cleanly: receptive knowledge of
+;; **lemma–surface-form pairs** in a fixed, versioned pool.
+;;
+;; Put differently, the destination is **estimated receptive Polish lemmas**,
+;; while pair knowledge is the first deliberately limited research target. That
+;; separation keeps the learner's purpose visible without pretending the hard
+;; linguistic bridge has already been built. It also lets each later article
+;; introduce one necessary decision when the reader has a concrete reason to
+;; care about it.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.purpose-opening
+ [:p "The number is the last step, not the product. The product is a defensible connection between a learner's answers and a clearly named quantity, with uncertainty and limitations visible."]])
+
+;; ## The learner's question {#learner-question}
+;;
+;; A learner does not arrive asking for an estimand, a sampling design, or a
+;; posterior distribution. They ask something like, “How much Polish can I
+;; understand?” LexiBench narrows that broad question to passive vocabulary:
+;; recognising Polish word meanings in sentence context. The production start
+;; screen explains that receptive ability in ordinary language, shows the test
+;; format, and asks for a starting level. That level is a routing hint for the
+;; first questions, not the final answer and not proof that a learner belongs
+;; to a CEFR category.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:figure.purpose-capture
+ [:img
+ {:src "lexibench_captures/lexibench-2026-07-17-starting-level.jpg"
+ :alt "LexiBench start screen explaining passive vocabulary and offering seven starting-level choices before an 80-item test."}]
+ [:figcaption
+ [:strong "Production start screen, captured 17 July 2026. "]
+ "The start screen defines passive vocabulary, asks for a starting level from pre-A1 through C2, and says the test has 80 sentence-context items."
+ [:span.purpose-annotation "The important annotation is conceptual: self-reported level selects where the experience begins; it does not become the vocabulary estimate."]]])
+
+;; Three layers are already visible. First is the **learner goal**: recognise
+;; meaning while reading or listening. Second is the **task**: choose the
+;; highlighted form’s English meaning inside a Polish sentence. Third is an
+;; **operational choice**: use a starting-level answer to avoid beginning in an
+;; obviously unhelpful region. None of those layers alone specifies a scorer.
+;; The scorer still needs to say which population the questions represent and
+;; how evidence from answered items becomes a statement about unasked ones.
+;;
+;; ## What one item hides {#what-item-hides}
+;;
+;; The next screen feels equally direct. A sentence contains one highlighted
+;; Polish form. The learner selects one English meaning or explicitly says
+;; “don't know.” Before anyone answers, however, a complete item has already
+;; embodied many decisions: the target lemma, the exact surface form, the
+;; intended meaning in this context, the sentence, its translation, the
+;; correct option, the distractors, and the version of every component.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:figure.purpose-capture
+ [:img
+ {:src "lexibench_captures/lexibench-2026-07-17-unanswered-sentence-item.jpg"
+ :alt "Unanswered LexiBench sentence item at zero of eighty, with one highlighted Polish form, five English choices, and a don't-know option."}]
+ [:figcaption
+ [:strong "Live production item, captured unanswered on 17 July 2026. "]
+ "Before any response, the live item shows progress at 0/80, asks for the highlighted Polish form’s meaning in its sentence, offers five translations, and keeps a separate don't-know response."
+ [:span.purpose-annotation "No answer was submitted for this capture. A screenshot can show the experience; it cannot establish that the item is unbiased, well calibrated, or equally informative for every learner."]]])
+
+;; A correct response is evidence, not a perfect observation of knowledge. A
+;; learner can guess. They can slip despite knowing the word. A weak distractor
+;; can make an item too easy; an ambiguous translation can make it unfair. A
+;; sentence can disambiguate a useful meaning or accidentally supply the
+;; answer. “Don't know” is valuable evidence and should remain distinct from a
+;; confidently chosen wrong answer in the stored event, even if a particular
+;; proposal later combines both outcomes for one calculation.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.purpose-question-grid
+ [:section.purpose-card
+ [:h3 "Language unit"]
+ [:p "Which lemma and which written or spoken form does the item represent?"]]
+ [:section.purpose-card
+ [:h3 "Meaning"]
+ [:p "Which interpretation does the sentence intend, and is the translation defensible?"]]
+ [:section.purpose-card
+ [:h3 "Alternatives"]
+ [:p "Do distractors reveal recognition without rewarding elimination tricks?"]]
+ [:section.purpose-card
+ [:h3 "Evidence"]
+ [:p "How do correct, wrong, and don't-know responses change uncertainty?"]]])
+
+;; ## Why count pairs first {#pair-stepping-stone}
+;;
+;; The eventual learner-facing unit is a lemma, but Polish lemmas appear through
+;; inflected surface forms. Recognising one common form does not automatically
+;; prove recognition of every case, number, gender, tense, person, or participle
+;; associated with the headword. Conversely, requiring every theoretically
+;; possible form would confuse vocabulary knowledge with exhaustive morphology
+;; and would over-weight rare forms. Context and meaning complicate the link
+;; further.
+;;
+;; That is why the first research estimand is deliberately modest: **receptive
+;; knowledge of lemma–surface-form pairs in a fixed, versioned pool**. A pair is
+;; more precise than “a word.” It lets an item point to a stable lemma identifier
+;; and a stable form identifier. A fixed pool supplies a denominator. Versioning
+;; means a changed inclusion rule or repaired item creates a new claim rather
+;; than silently changing the meaning of an old result.
+;;
+;; The pair count is a stepping stone, not the promised destination. It helps
+;; isolate the sampling and inference problem: given a declared population of
+;; pairs and a small set of responses, how many pairs might this learner
+;; recognise? Once that layer is credible, later research can ask how evidence
+;; across correlated forms supports a latent statement about lemma knowledge.
+;; Jumping directly to lemma totals would hide that unresolved bridge inside a
+;; seemingly friendly number.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.purpose-callout.warm
+ [:strong "A synthetic pool is not the production inventory"]
+ [:p "Proposal 1 and Proposal 2 use a synthetic 8,000-pair pool to make model behaviour executable and comparable. It is not the live LexiBench lemma inventory, not a CEFR inventory, and not evidence that Polish has exactly 8,000 relevant pairs."]])
+
+;; ## The complete measurement chain {#measurement-chain}
+;;
+;; A short test can be useful only when the whole chain is visible enough to
+;; challenge. Optimising the final formula while leaving the pool or items
+;; vague would merely make an undefined construct faster. The direction of
+;; travel is the shortest test that meets broad quality standards: it should
+;; measure the intended construct, limit avoidable bias and error, report
+;; calibrated uncertainty, and remain robust across relevant learner and item
+;; groups. Length is optimised only among candidates that pass those standards.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.purpose-chain
+ [:section.purpose-chain-step
+ [:h3 "Construct and version the pool"]
+ [:p "Define which lemma–form pairs can be counted, their identifiers, inclusion rules, frequencies, and immutable version."]]
+ [:section.purpose-chain-step
+ [:h3 "Select administered items"]
+ [:p "Sample broadly and reproducibly while controlling repeats, exposure, and learner burden."]]
+ [:section.purpose-chain-step
+ [:h3 "Design each complete item"]
+ [:p "Version the context, intended meaning, translation, answer, distractors, and links to the target pair."]]
+ [:section.purpose-chain-step
+ [:h3 "Pilot and calibrate"]
+ [:p "Use representative learner evidence to diagnose ambiguity, bias, difficulty, guessing, slips, and weak distractors."]]
+ [:section.purpose-chain-step
+ [:h3 "Infer knowledge and uncertainty"]
+ [:p "Connect stored responses to the declared pool with a versioned scorer and an honest uncertainty statement."]]
+ [:section.purpose-chain-step
+ [:h3 "Validate the interpretation"]
+ [:p "Check whether results support their intended use across groups, versions, and realistic operating conditions."]]])
+
+;; These stages depend on one another. Pool frequency can help spread a sample,
+;; but frequency is not measured learner difficulty. A statistically neat
+;; interval does not repair an ambiguous item. Passing software tests proves
+;; that code behaves as specified; it does not prove that the specification
+;; measures vocabulary. A polished published page proves neither. Model,
+;; software, and publication checks therefore remain separate lanes.
+;;
+;; ## Three scorers, three statuses {#three-scorers}
+;;
+;; The production interface and the research proposals must not be blended.
+;; The **Current LexiBench scorer** estimates recognised Polish lemmas from the
+;; live 10,000-lemma inventory and exposes a likely range. It is the scorer
+;; behind the current product shown here. This article documents its learner
+;; purpose and published interface, not its formula.
+;;
+;; **Proposal 1**, exact algorithm ID `stratified-beta-binomial-v1`, is the
+;; current research implementation target in this public workflow. It divides
+;; a synthetic pair pool into broad frequency strata, samples non-adaptively,
+;; updates uncertainty from responses, predicts knowledge in untested pairs,
+;; and recommends stopping at complete rounds. The worked example estimates
+;; 4,334 known pairs. That result belongs to the synthetic fixture, not to a
+;; production learner or inventory.
+;;
+;; **Proposal 2**, exact algorithm ID
+;; `continuous-pair-frequency-logistic-v2`, is a preserved experimental
+;; checkpoint. It asked whether a continuous frequency curve could improve
+;; accuracy and shorten tests. It improved some aggregate measures but failed
+;; precommitted worst-group and median-length gates, so it was not promoted.
+;; Retaining the candidate and its evidence is part of the method: an honest
+;; rejection should remain reproducible instead of disappearing.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:table.purpose-status-table
+ [:caption "What each visible or published scorer means"]
+ [:thead
+ [:tr
+ [:th {:scope "col"} "Name"]
+ [:th {:scope "col"} "Quantity"]
+ [:th {:scope "col"} "Status"]]]
+ [:tbody
+ [:tr
+ [:th {:scope "row"} "Current LexiBench scorer"]
+ [:td "Estimated recognised Polish lemmas"]
+ [:td "Deployed in the current product"]]
+ [:tr
+ [:th {:scope "row"} "Proposal 1 · stratified-beta-binomial-v1"]
+ [:td "Known lemma–surface-form pairs in a fixed synthetic pool"]
+ [:td "Current research target; not deployed"]]
+ [:tr
+ [:th {:scope "row"} "Proposal 2 · continuous-pair-frequency-logistic-v2"]
+ [:td "Known lemma–surface-form pairs in the same synthetic study"]
+ [:td "Not promoted; not deployed"]]]])
+
+;; **Neither proposal is deployed.** Calling Proposal 1 “current” means current
+;; within this research programme, not current in LexiBench. Calling Proposal 2
+;; “published” means its article, code, and evidence are inspectable, not that
+;; its model powers the test. The distinction matters because production
+;; screenshots can otherwise make a research formula look operational, while
+;; an executable research article can make a synthetic example look like real
+;; learner evidence.
+;;
+;; ## The learner-facing destination {#learner-destination}
+;;
+;; The production scoring example shows why the longer chain is worth the
+;; trouble. A useful result names the unit, gives a centre estimate, and shows a
+;; range rather than claiming exact knowledge. It can also explain which parts
+;; of the inventory were directly observed and which were inferred. That is far
+;; more informative than a bare “words known” number.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:figure.purpose-capture
+ [:img
+ {:src "lexibench_captures/lexibench-2026-07-17-published-scoring-example.jpg"
+ :alt "Published LexiBench scoring example showing a live and final estimate of about 1,450 recognized Polish lemmas with a likely range of 1,050 to 1,900."}]
+ [:figcaption
+ [:strong "Published current-product example, captured 17 July 2026. "]
+ "The published example reports about 1,450 recognized Polish lemmas, shows 1,050–1,900 as the likely range, and labels the result approximate rather than exact."
+ [:span.purpose-annotation "This is a static example on LexiBench's scoring documentation page, not a fabricated completed test and not a result produced by either research proposal."]]])
+
+;; The eventual research question is how to support an estimate of **estimated
+;; receptive Polish lemmas** with evidence gathered from particular forms and
+;; contexts. This series does not pretend that question is solved. It first
+;; teaches the workflow, then the purpose, then the probability ideas needed to
+;; reason from a sample. Proposal 1 demonstrates a transparent pair-count
+;; model. Proposal 2 records a more ambitious candidate and why its evidence
+;; was insufficient for promotion.
+;;
+;; Several decisions remain explicitly deferred. We have not decided the rule
+;; that converts correlated pair evidence into latent lemma knowledge. We have
+;; not finished representative item calibration or the detailed sampling rules
+;; for how many forms and contexts each lemma needs. We have not earned an
+;; adaptive selector: adaptation should follow calibrated items and exposure
+;; controls, not treat raw frequency as difficulty. We have not resolved when
+;; multiple senses or contexts become separately identifiable. Those are not
+;; footnotes to hide; they are the research roadmap.
+
+^:kindly/hide-code
+(kind/hiccup
+ [:div.purpose-callout.green
+ [:strong "The standard for progress"]
+ [:p "Compare versioned candidates with clear baselines; distinguish simulations from representative learner evidence; preserve failed checkpoints; and claim only what the evidence supports. “Most accurate possible” is a direction, not a claim that one globally optimal vocabulary test has been found."]])
+
+;; ## Technical boundaries {#technical-boundaries}
+;;
+;; This orientation adds no new statistical derivation and runs no simulation.
+;; Its technical role is to name the interfaces between later articles. The
+;; exact research estimand is pair knowledge in a declared pool. Raw response
+;; events preserve correct, wrong, and don't-know outcomes. Inputs, selectors,
+;; algorithms, random seeds, and outputs are versioned for replay. Proposal 1
+;; and Proposal 2 remain historical research objects with their existing
+;; contracts and evidence unchanged.
+;;
+;; ## Capture provenance {#capture-provenance}
+;;
+;; The three images are dated, read-only captures of the production site. The
+;; start and question screens show current interactive states; the score is the
+;; site's published static example. No fabricated 80-answer production session
+;; was submitted. Source URL, recapture state, capture date, product status,
+;; alternative text, prose equivalent, media type, and SHA-256 digest are stored
+;; beside the immutable image files in
+;; `lexibench-captures-2026-07-17.edn`. A later UI change should produce a new
+;; dated capture set, not overwrite this evidence.
+;; END ALWAYS-VISIBLE
+
+^:kindly/hide-code
+(kind/hiccup
+ [:details.purpose-technical
+ [:summary "Source and reproducibility links"]
+ [:div.purpose-technical-body
+ [:p "The reader-facing boundaries above are backed by the versioned implementation target, executable articles, tests, and preserved evidence in the public workflow."]
+ [:ul
+ [:li [:a {:href "https://github.com/jamiepratt/theory-to-algorithm-workflow/blob/master/docs/language-learning/vocabulary-estimation/current-scoring-algorithm.md"}
+ "Proposal 1 scoring contract"]]
+ [:li [:a {:href "beta_binomial_first_pass.html"}
+ "Proposal 1 executable article"]]
+ [:li [:a {:href "pair_frequency_logistic_v2_article.html"}
+ "Proposal 2 experiment and non-promotion decision"]]
+ [:li [:a {:href "https://lexibench.com/#/current/scoring"}
+ "Current LexiBench scoring explanation"]]]
+ [:p.purpose-source-note "Technical disclosure preserves provenance without turning this orientation into another derivation or simulation."]]])
diff --git a/test/language_learning/vocabulary_estimation/article_controls_test.cljs b/test/language_learning/vocabulary_estimation/article_controls_test.cljs
index 717d5a2b..b9c13c69 100644
--- a/test/language_learning/vocabulary_estimation/article_controls_test.cljs
+++ b/test/language_learning/vocabulary_estimation/article_controls_test.cljs
@@ -27,3 +27,60 @@
(deftest namespace-loads-without-a-dom-test
(is (false? (controls/dom-available?))))
+
+(deftest drawer-state-and-focus-contract-test
+ (is (= {:open? true :focus :drawer-close}
+ (controls/drawer-transition {:open? false} :open)))
+ (is (= {:open? false :focus :trigger}
+ (controls/drawer-transition {:open? true} :close)))
+ (is (= {:open? false :focus :trigger}
+ (controls/drawer-transition {:open? true} :escape)))
+ (is (= {:open? false :focus nil}
+ (controls/drawer-transition {:open? false} :escape))))
+
+(deftest active-section-follows-hash-then-scroll-position-test
+ (let [sections [{:id "question" :top -180}
+ {:id "chain" :top 48}
+ {:id "limits" :top 260}]]
+ (is (= "limits" (controls/active-section-id sections "#limits" 96)))
+ (is (= "chain" (controls/active-section-id sections "" 96)))
+ (is (= "chain" (controls/active-section-id sections "#missing" 96)))
+ (is (= "question"
+ (controls/active-section-id
+ [{:id "question" :top 250}
+ {:id "chain" :top 520}]
+ "" 96)))))
+
+(deftest anchor-navigation-leaves-history-to-the-native-anchor-test
+ (is (= {:hash "#measurement-chain"
+ :history :native
+ :scroll :smooth
+ :focus "measurement-chain"}
+ (controls/anchor-navigation "measurement-chain" false)))
+ (is (= :instant
+ (:scroll (controls/anchor-navigation "measurement-chain" true)))))
+
+(deftest section-id-is-derived-from-rendered-link-href-test
+ (is (= "measurement-chain"
+ (controls/section-id-from-href "#measurement-chain")))
+ (is (= "measurement-chain"
+ (controls/section-id-from-href
+ "http://localhost/article.html#measurement-chain")))
+ (is (nil? (controls/section-id-from-href "http://localhost/article.html")))
+ (is (nil? (controls/section-id-from-href "#"))))
+
+(deftest global-key-handling-ignores-editable-contexts-test
+ (doseq [context [{:tag-name "input"}
+ {:tag-name "TEXTAREA"}
+ {:tag-name "select"}
+ {:content-editable? true}
+ {:role "textbox"}]]
+ (is (false? (controls/global-key-handled? "Escape" context true))))
+ (is (true? (controls/global-key-handled?
+ "Escape" {:tag-name "DIV"} true)))
+ (is (true? (controls/global-key-handled?
+ "Escape" {:tag-name "BUTTON"} true)))
+ (is (false? (controls/global-key-handled?
+ "ArrowDown" {:tag-name "DIV"} true)))
+ (is (false? (controls/global-key-handled?
+ "Escape" {:tag-name "DIV"} false))))
diff --git a/test/language_learning/vocabulary_estimation/article_equation_code_test.clj b/test/language_learning/vocabulary_estimation/article_equation_code_test.clj
index 7bb56657..f36a41f8 100644
--- a/test/language_learning/vocabulary_estimation/article_equation_code_test.clj
+++ b/test/language_learning/vocabulary_estimation/article_equation_code_test.clj
@@ -1,5 +1,6 @@
(ns language-learning.vocabulary-estimation.article-equation-code-test
- (:require [clojure.java.io :as io]
+ (:require [clojure.edn :as edn]
+ [clojure.java.io :as io]
[clojure.string :as str]
[clojure.test :refer [deftest is testing]]
[language-learning.vocabulary-estimation.article-controls :as controls]))
@@ -16,6 +17,17 @@
(defn occurrence-count [pattern text]
(count (re-seq pattern text)))
+(defn file-sha-256 [file]
+ (let [digest (java.security.MessageDigest/getInstance "SHA-256")]
+ (with-open [input (io/input-stream file)]
+ (let [buffer (byte-array 8192)]
+ (loop []
+ (let [read-count (.read input buffer)]
+ (when (pos? read-count)
+ (.update digest buffer 0 read-count)
+ (recur))))))
+ (format "%064x" (BigInteger. 1 (.digest digest)))))
+
(deftest every-display-equation-has-code-provenance-and-symbol-help
(doseq [file (authored-article-files)
:let [source (slurp file)
@@ -114,11 +126,230 @@
(is (str/includes? (second (first styles))
".article-equation-title-icon"))))
+(deftest series-manifest-has-a-stable-five-article-opening
+ (is (= [:workflow :purpose :bayes :proposal-1 :proposal-2]
+ (mapv :id controls/series-manifest)))
+ (is (= [1 2 3 4 5]
+ (mapv :number controls/series-manifest)))
+ (is (every? #(every? (set (keys %))
+ [:id :number :title :url :status])
+ controls/series-manifest))
+ (is (seq controls/series-roadmap))
+ (is (every? #(and (:id %) (:title %) (:status %) (nil? (:number %)))
+ controls/series-roadmap)))
+
+(deftest configured-controls-render-series-context-and-active-contents-data
+ (let [configured
+ (controls/install
+ {:article-id :purpose
+ :sections [{:id "learner-question"
+ :label "The learner's question"}]
+ :technical-sections [{:id "technical-depth"
+ :label "Technical depth"}]})
+ nodes (filter vector? (tree-seq coll? seq configured))
+ node-by-id
+ (fn [id]
+ (some #(when (and (map? (second %))
+ (= id (:id (second %))))
+ %)
+ nodes))
+ links (filter #(= :a (first %)) nodes)
+ current (some #(when (= "page" (get-in % [1 :aria-current])) %) nodes)
+ json-node (node-by-id "article-controls-config")
+ drawer (some #(when (= :aside#article-contents-drawer.article-contents-drawer
+ (first %))
+ %)
+ nodes)
+ style (second (some #(when (= :style (first %)) %) nodes))]
+ (is (= :purpose (get-in (controls/series-context :purpose) [:current :id])))
+ (is (= :workflow (get-in (controls/series-context :purpose) [:previous :id])))
+ (is (= :bayes (get-in (controls/series-context :purpose) [:next :id])))
+ (is (some #(= "managing_brilliant_but_uneven_minds.html"
+ (get-in % [1 :href])) links))
+ (is (some #(= "bayes_theorem_simulations.html"
+ (get-in % [1 :href])) links))
+ (is (= "Article 2 of 5" (last current)))
+ (is (= 2 (count (filter #(= "page" (get-in % [1 :aria-current]))
+ nodes))))
+ (is (some #(= :details.series-roadmap (first %)) nodes))
+ (is (= "application/json" (get-in json-node [1 :type])))
+ (is (str/includes? (last json-node) "\"articleId\":\"purpose\""))
+ (is (str/includes? (last json-node) "\"technical\":true"))
+ (is (= "dialog" (get-in drawer [1 :role])))
+ (is (true? (get-in drawer [1 :hidden])))
+ (is (str/includes? style "prefers-reduced-motion: reduce"))))
+
+(deftest desktop-contents-rail-clears-the-article-and-viewport-edges
+ (let [style (slurp (io/file "resources" "language_learning"
+ "vocabulary_estimation" "article_controls.css"))
+ rem-value
+ (fn [property]
+ (some-> (re-find (re-pattern
+ (str property ":\\s*([0-9.]+)rem"))
+ style)
+ second
+ Double/parseDouble))
+ content-half-width (rem-value "--article-content-half-width")
+ contents-gap (rem-value "--article-contents-gap")
+ contents-width (rem-value "--article-contents-width")
+ viewport-gap (rem-value "--article-contents-viewport-gap")
+ root-pixels 17.0]
+ (is (every? some? [content-half-width contents-gap contents-width
+ viewport-gap])
+ "Desktop contents geometry must name its article, rail, and edge bounds")
+ (is (str/includes?
+ style
+ "left: calc(50% + var(--article-content-half-width) + var(--article-contents-gap));"))
+ (is (str/includes?
+ style
+ "width: min(var(--article-contents-width), calc(50vw - var(--article-content-half-width) - var(--article-contents-gap) - var(--article-contents-viewport-gap)));"))
+ (when (every? some? [content-half-width contents-gap contents-width
+ viewport-gap])
+ (doseq [viewport-width [1280.0 1366.0 1440.0 1600.0]
+ :let [article-right (+ (/ viewport-width 2.0)
+ (* content-half-width root-pixels))
+ rail-left (+ article-right (* contents-gap root-pixels))
+ available-width (- (/ viewport-width 2.0)
+ (* (+ content-half-width contents-gap
+ viewport-gap)
+ root-pixels))
+ rail-rendered-width (min (* contents-width root-pixels)
+ available-width)
+ rail-right (+ rail-left rail-rendered-width)]]
+ (is (<= article-right rail-left)
+ (str "Contents rail overlaps the article at " viewport-width "px"))
+ (is (<= rail-right (- viewport-width (* viewport-gap root-pixels)))
+ (str "Contents rail crosses the viewport gutter at "
+ viewport-width "px"))))))
+
+(deftest purpose-article-is-a-bounded-standalone-orientation
+ (let [source (slurp (io/file authored-root "why_estimate_vocabulary.clj"))
+ visible-region (second (re-find
+ #"(?s)BEGIN ALWAYS-VISIBLE(.*?)END ALWAYS-VISIBLE"
+ source))
+ visible-prose (->> (str/split-lines visible-region)
+ (keep #(second (re-find #"^;;\s?(.*)$" %)))
+ (str/join " "))
+ word-count (count (re-seq #"[\p{L}\p{N}]+(?:[’'-][\p{L}\p{N}]+)*"
+ visible-prose))]
+ (is (<= 1500 word-count 2200)
+ (str "Always-visible Article 2 prose has " word-count " words."))
+ (doseq [contract ["Current LexiBench scorer"
+ "Proposal 1"
+ "stratified-beta-binomial-v1"
+ "Proposal 2"
+ "continuous-pair-frequency-logistic-v2"
+ "Neither proposal is deployed"
+ "synthetic 8,000-pair pool"
+ "estimated receptive Polish lemmas"
+ "lemma–surface-form pairs"
+ "calibration"
+ "adaptive"]]
+ (is (str/includes? source contract)
+ (str "Missing Article 2 boundary: " contract)))
+ (is (str/includes? source
+ "(controls/install\n {:article-id :purpose"))))
+
+(deftest workflow-article-owns-the-management-method-not-the-product-purpose
+ (let [source (slurp (io/file authored-root
+ "managing_brilliant_but_uneven_minds.clj"))]
+ (doseq [contract ["brilliant but profoundly uneven minds"
+ "The management job"
+ "The theory-to-algorithm cycle"
+ "Workflow validation"
+ "Model validation"
+ "Software validation"
+ "Publication validation"
+ "human responsibility"
+ "I have not yet read"
+ "why_estimate_vocabulary.html"]]
+ (is (str/includes? source contract)
+ (str "Missing Article 1 management contract: " contract)))
+ (is (str/includes? source
+ "(controls/install\n {:article-id :workflow")
+ "Article 1 must use canonical 1-based series and contents controls")
+ (is (= 1 (occurrence-count #"(?i)LexiBench" source))
+ "Article 1 should retain one short LexiBench bridge, not its purpose essay")
+ (doseq [duplicated-purpose-section
+ ["## From an expectation to an estimand"
+ "Hidden choices behind a vocabulary-size result"
+ "The current first-pass estimand is deliberately narrower"
+ "## Theory already used, and theory still to earn"]]
+ (is (not (str/includes? source duplicated-purpose-section))
+ (str "Article 2 material remains duplicated: "
+ duplicated-purpose-section)))
+ (is (not (str/includes? source "[:nav.series-toc"))
+ "Article 1 must not retain its hand-built zero-based series list")))
+(deftest bayes-article-adopts-the-series-shell-and-retains-four-simulations
+ (let [article (slurp (io/file authored-root
+ "bayes_theorem_simulations.clj"))
+ interactive (slurp (io/file authored-root
+ "bayes_theorem_simulations_interactive.cljs"))]
+ (is (str/includes? article
+ ":title \"Bayes' theorem: from uncertainty to decision\""))
+ (is (str/includes? article ":subtitle"))
+ (is (str/includes? article
+ "(controls/install\n {:article-id :bayes"))
+ (is (not (re-find #":label \"[1-5]\." article))
+ "Ordered contents must not duplicate chapter numbers inside labels")
+ (doseq [mount-id ["globe-update-simulator"
+ "posterior-sampling-simulator"
+ "gaussian-height-simulator"
+ "vocabulary-pair-simulator"]]
+ (testing mount-id
+ (is (str/includes? article mount-id))
+ (is (str/includes? interactive mount-id))))
+ (doseq [boundary ["Beta(1,1)"
+ "100-pair"
+ "lemma–surface-form pairs"
+ "teaching and model-behavior demonstration"
+ "not learner validation"
+ "does not implement or change Proposal 1"]]
+ (is (str/includes? article boundary)
+ (str "Missing Bayes bridge boundary: " boundary)))))
+
+(deftest bayes-predictive-helper-results-stay-out-of-visible-prose
+ (let [article (slurp (io/file authored-root
+ "bayes_theorem_simulations.clj"))]
+ (doseq [helper ["seeded-uniform!"
+ "integer-gamma-sample!"
+ "beta-sample!"
+ "binomial-sample!"]]
+ (is (re-find
+ (re-pattern
+ (str "\\^\\{:kindly/hide-code true\\s+"
+ ":kindly/kind :kind/hidden\\}\\s+"
+ "\\(defn- " (java.util.regex.Pattern/quote helper)))
+ article)
+ (str helper " must not emit its Var into the rendered article")))))
+(deftest purpose-article-production-captures-are-immutable-and-described
+ (let [capture-root (io/file authored-root "lexibench_captures")
+ manifest-file (io/file capture-root
+ "lexibench-captures-2026-07-17.edn")
+ captures (:captures (edn/read-string (slurp manifest-file)))
+ article (slurp (io/file authored-root "why_estimate_vocabulary.clj"))]
+ (is (= 3 (count captures)))
+ (doseq [{:keys [file sha-256 source-url capture-date product-status
+ recapture-state alt prose-equivalent]} captures
+ :let [image-file (io/file capture-root file)]]
+ (testing file
+ (is (.exists image-file))
+ (is (= sha-256 (file-sha-256 image-file)))
+ (is (= "2026-07-17" capture-date))
+ (is (= :current-production product-status))
+ (is (str/starts-with? source-url "https://lexibench.com/"))
+ (is (not (str/blank? recapture-state)))
+ (is (<= 18 (count alt)))
+ (is (<= 30 (count prose-equivalent)))
+ (is (str/includes? article file))
+ (is (str/includes? article alt))
+ (is (str/includes? article prose-equivalent))))))
+
(deftest every-authored-article-installs-controls-once
(doseq [file (authored-article-files)
:let [source (slurp file)]]
(testing (.getPath file)
- (is (= 1 (occurrence-count #"\(controls/install\)" source)))
+ (is (= 1 (occurrence-count #"\(controls/install(?:\)|\s)" source)))
(is (str/includes?
source
"[language-learning.vocabulary-estimation.article-controls :as controls]"))
@@ -262,10 +493,10 @@
"Theory-to-algorithm research cycle"]
["bayes_theorem_simulations.clj"
"bayes_theorem_simulations_preview.png"
- "Three Gaussian parameter-grid heatmaps"]
+ "A finite-pool posterior-predictive chart"]
["beta_binomial_first_pass.clj"
"beta_binomial_first_pass_preview.png"
- "Beta-binomial posterior density"]
+ "Proposal 1 article title and subtitle"]
["pair_frequency_logistic_v2_article.clj"
"pair_frequency_logistic_v2_posterior_preview.png"
"Cell-failure explorer"]]
@@ -293,3 +524,63 @@
(is (str/includes?
article
"worst cell achieved only 92.4% coverage—462 of 500 intervals"))))
+
+(deftest proposal-1-article-follows-one-seeded-learner-attempt
+ (let [article (slurp (io/file authored-root
+ "beta_binomial_first_pass.clj"))
+ section-anchors ["{#attempt-target}"
+ "{#attempt-selection}"
+ "{#attempt-responses}"
+ "{#attempt-update}"
+ "{#attempt-prediction}"
+ "{#attempt-stopping}"
+ "{#attempt-replay}"]
+ anchor-offsets (mapv #(.indexOf article %) section-anchors)]
+ (is (str/includes? article
+ ":title \"Proposal 1: estimating known pairs\""))
+ (is (str/includes? article ":subtitle"))
+ (is (str/includes? article
+ "(controls/install\n {:article-id :proposal-1"))
+ (is (not (str/includes? article "[:nav.series-toc"))
+ "Proposal 1 must use the canonical 1-based series navigation")
+ (is (every? #(<= 0 %) anchor-offsets)
+ "Every learner-attempt stage must have a direct anchor")
+ (is (apply < anchor-offsets)
+ "The always-visible narrative must follow selection through replay")
+ (doseq [contract ["one seeded synthetic learner attempt"
+ "current research implementation target"
+ "`stratified-beta-binomial-v1`"
+ "not the scorer currently deployed at Lexibench"
+ "receptive knowledge of lemma–surface-form pairs"
+ "frequency rank is a proxy"
+ "adaptive selection"
+ "later work after item calibration"]]
+ (is (str/includes? article contract)
+ (str "Missing Proposal 1 boundary: " contract)))))
+
+(deftest proposal-1-retains-the-reference-fixture-and-all-four-interactions
+ (let [article (slurp (io/file authored-root
+ "beta_binomial_first_pass.clj"))
+ interactive (slurp (io/file authored-root
+ "beta_binomial_first_pass_interactive.cljs"))]
+ (doseq [reference ["8,000-pair"
+ "4,334"
+ "3,404–5,249"
+ ":algorithm-id :stratified-beta-binomial-v1"
+ ":seed 20260712"
+ ":response :correct"
+ ":response :wrong"
+ ":response :dont-know"]]
+ (is (str/includes? article reference)
+ (str "Missing Proposal 1 reference contract: " reference)))
+ (doseq [mount-id ["balanced-round-simulator"
+ "beta-binomial-simulator"
+ "posterior-sampling-simulator"
+ "stopping-rule-explorer"]]
+ (testing mount-id
+ (is (str/includes? article mount-id))
+ (is (str/includes? interactive mount-id))))
+ (is (str/includes? interactive "(def seeded-attempt-selection-limit 32)")
+ "The fixed schedule must reach the 32-event reference attempt")
+ (is (str/includes? article ":raw-responses seeded-attempt-events")
+ "The replay fixture must preserve the original three-way events")))
diff --git a/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive_test.cljs b/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive_test.cljs
new file mode 100644
index 00000000..47637405
--- /dev/null
+++ b/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_interactive_test.cljs
@@ -0,0 +1,43 @@
+(ns language-learning.vocabulary-estimation.bayes-theorem-simulations-interactive-test
+ (:require [cljs.test :refer-macros [deftest is testing]]
+ [language-learning.vocabulary-estimation.bayes-theorem-simulations-interactive :as bayes]))
+
+(deftest browser-bridge-updates-and-predicts-the-finite-pool
+ (is (= {:alpha 8 :beta 4}
+ (bayes/beta-posterior-parameters
+ {:alpha 1 :beta 1}
+ {:recognized 7 :not-recognized 3})))
+ (let [summary (bayes/finite-pool-predictive-summary
+ {:pool-size 100
+ :recognized 7
+ :not-recognized 3
+ :prior-alpha 1
+ :prior-beta 1
+ :draw-count 4000
+ :seed 620260717})]
+ (is (= 67 (:mean summary)))
+ (is (<= 7 (:lower summary) (:mean summary) (:upper summary) 97))
+ (is (= 4000 (reduce + (vals (:frequencies summary)))))))
+
+(deftest bridge-reset-replays-identical-prediction
+ (bayes/reset-vocabulary-bridge!)
+ (bayes/record-pair-response! :recognized)
+ (bayes/record-pair-response! :not-recognized)
+ (let [first-run (bayes/current-vocabulary-bridge-summary)]
+ (bayes/reset-vocabulary-bridge!)
+ (bayes/record-pair-response! :recognized)
+ (bayes/record-pair-response! :not-recognized)
+ (testing "the same responses after reset reproduce every seeded result"
+ (is (= first-run (bayes/current-vocabulary-bridge-summary))))))
+
+(deftest bridge-exposes-binary-keyboard-operable-controls-and-a-live-result
+ (bayes/reset-vocabulary-bridge!)
+ (let [component (bayes/vocabulary-bridge-simulator)
+ nodes (filter vector? (tree-seq coll? seq component))
+ buttons (filter #(= bayes/control-button (first %)) nodes)
+ section (first component)]
+ (is (= :section.bp-shell section))
+ (is (some #(= "Recognized" (second %)) buttons))
+ (is (some #(= "Not recognized" (second %)) buttons))
+ (is (some #(= "Reset" (second %)) buttons))
+ (is (some #(= "polite" (get-in % [1 :aria-live])) nodes))))
diff --git a/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_test.clj b/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_test.clj
new file mode 100644
index 00000000..1fd12c7d
--- /dev/null
+++ b/test/language_learning/vocabulary_estimation/bayes_theorem_simulations_test.clj
@@ -0,0 +1,28 @@
+(ns language-learning.vocabulary-estimation.bayes-theorem-simulations-test
+ (:require [clojure.test :refer [deftest is testing]]
+ [language-learning.vocabulary-estimation.bayes-theorem-simulations :as bayes]))
+
+(deftest binary-pair-responses-update-a-declared-beta-prior
+ (is (= {:alpha 8.0 :beta 4.0}
+ (bayes/beta-posterior-parameters
+ {:alpha 1.0 :beta 1.0}
+ {:recognized 7 :not-recognized 3}))))
+
+(deftest seeded-posterior-prediction-covers-the-finite-pair-pool
+ (let [options {:pool-size 100
+ :recognized 7
+ :not-recognized 3
+ :prior-alpha 1.0
+ :prior-beta 1.0
+ :draw-count 4000
+ :seed 620260717}
+ first-run (bayes/seeded-finite-pool-predictive options)
+ replay (bayes/seeded-finite-pool-predictive options)]
+ (testing "the posterior predictive mean includes observed known pairs"
+ (is (< (Math/abs (- 67.0 (:mean first-run))) 1.0e-12)))
+ (testing "the seeded interval is deterministic and respects pool bounds"
+ (is (= first-run replay))
+ (is (<= 7 (:lower first-run) (:upper first-run) 97))
+ (is (<= (:lower first-run) (:mean first-run) (:upper first-run))))
+ (testing "the complete seeded distribution remains inspectable"
+ (is (= 4000 (reduce + (vals (:frequencies first-run))))))))
diff --git a/test/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive_test.cljs b/test/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive_test.cljs
new file mode 100644
index 00000000..a5128474
--- /dev/null
+++ b/test/language_learning/vocabulary_estimation/beta_binomial_first_pass_interactive_test.cljs
@@ -0,0 +1,64 @@
+(ns language-learning.vocabulary-estimation.beta-binomial-first-pass-interactive-test
+ (:require [cljs.test :refer-macros [deftest is testing]]
+ [language-learning.vocabulary-estimation.beta-binomial-first-pass-interactive :as proposal-1]))
+
+(deftest seeded-attempt-is-balanced-non-adaptive-and-lossless
+ (let [events (mapv proposal-1/scheduled-selection
+ (range proposal-1/seeded-attempt-selection-limit))
+ by-stratum (group-by :stratum events)]
+ (is (= 32 (count events)))
+ (is (= (set (range 1 9)) (set (keys by-stratum))))
+ (is (every? #(= 4 (count %)) (vals by-stratum)))
+ (is (= [4 4 3 3 2 1 1 0]
+ (mapv (fn [stratum]
+ (count (filter #(= :correct (:response %))
+ (get by-stratum stratum))))
+ (range 1 9))))
+ (is (= 32 (count (distinct (map :item-id events)))))
+ (is (= #{:correct :wrong :dont-know} (set (map :response events))))
+ (is (= proposal-1/seeded-stratum-orders
+ (mapv (fn [round]
+ (mapv :stratum
+ (filter #(= round (:round %)) events)))
+ (range 1 5))))
+ (is (every? #(not= (vec (range 1 9)) %)
+ proposal-1/seeded-stratum-orders)
+ "Every round retains a nontrivial seeded presentation shuffle")
+ (testing "selection is a fixed function of position, not prior responses"
+ (let [scheduled-before (proposal-1/scheduled-selection 17)]
+ (reset! proposal-1/state proposal-1/initial-state)
+ (doseq [response [:wrong :correct :dont-know :wrong]]
+ (proposal-1/record-response! response))
+ (is (= scheduled-before
+ (proposal-1/scheduled-selection 17)))))))
+
+(deftest three-way-responses-remain-raw-while-proposal-1-updates-binarily
+ (reset! proposal-1/state proposal-1/initial-state)
+ (doseq [response [:correct :wrong :dont-know]]
+ (proposal-1/record-response! response))
+ (is (= [:correct :wrong :dont-know] (:responses @proposal-1/state)))
+ (is (= {:alpha 2 :beta 3}
+ (select-keys @proposal-1/state [:alpha :beta]))))
+
+(deftest posterior-predictive-reset-replays-the-same-seeded-draw
+ (let [first-draw (proposal-1/sample-complete-draw!
+ (proposal-1/make-rng proposal-1/sampling-seed))
+ replay (proposal-1/sample-complete-draw!
+ (proposal-1/make-rng proposal-1/sampling-seed))]
+ (is (= first-draw replay))
+ (is (= 8 (count (:strata first-draw))))
+ (is (<= 18 (:total first-draw) 7986))))
+
+(deftest stopping-explorer-preserves-round-boundary-and-soft-cap-decisions
+ (is (false? (:assess?
+ (proposal-1/explorer-stopping-check
+ {:minimum 32 :target-percent 10 :soft-cap 96
+ :items-tested 36 :half-width 100}))))
+ (is (true? (:target?
+ (proposal-1/explorer-stopping-check
+ {:minimum 32 :target-percent 10 :soft-cap 96
+ :items-tested 40 :half-width 750}))))
+ (is (true? (:soft-max?
+ (proposal-1/explorer-stopping-check
+ {:minimum 32 :target-percent 10 :soft-cap 96
+ :items-tested 96 :half-width 900})))))
diff --git a/test/language_learning/vocabulary_estimation/cljs_test_runner.cljs b/test/language_learning/vocabulary_estimation/cljs_test_runner.cljs
index f6afec76..6203ef60 100644
--- a/test/language_learning/vocabulary_estimation/cljs_test_runner.cljs
+++ b/test/language_learning/vocabulary_estimation/cljs_test_runner.cljs
@@ -1,6 +1,8 @@
(ns language-learning.vocabulary-estimation.cljs-test-runner
(:require [cljs.test :as test]
[language-learning.vocabulary-estimation.article-controls-test]
+ [language-learning.vocabulary-estimation.bayes-theorem-simulations-interactive-test]
+ [language-learning.vocabulary-estimation.beta-binomial-first-pass-interactive-test]
[language-learning.vocabulary-estimation.pair-frequency-logistic-v2-test]))
(defmethod test/report [:cljs.test/default :end-run-tests] [m]
@@ -10,6 +12,8 @@
(defn -main []
(test/run-tests
'language-learning.vocabulary-estimation.article-controls-test
+ 'language-learning.vocabulary-estimation.bayes-theorem-simulations-interactive-test
+ 'language-learning.vocabulary-estimation.beta-binomial-first-pass-interactive-test
'language-learning.vocabulary-estimation.pair-frequency-logistic-v2-test))
(set! *main-cli-fn* -main)
diff --git a/test/language_learning/vocabulary_estimation/test_runner.clj b/test/language_learning/vocabulary_estimation/test_runner.clj
index 5dfea4b2..47b70a70 100644
--- a/test/language_learning/vocabulary_estimation/test_runner.clj
+++ b/test/language_learning/vocabulary_estimation/test_runner.clj
@@ -1,6 +1,7 @@
(ns language-learning.vocabulary-estimation.test-runner
(:require [clojure.test :as test]
[language-learning.vocabulary-estimation.article-equation-code-test]
+ [language-learning.vocabulary-estimation.bayes-theorem-simulations-test]
[language-learning.vocabulary-estimation.pair-frequency-logistic-v2-fixture-test]
[language-learning.vocabulary-estimation.pair-frequency-logistic-v2-gate-test]
[language-learning.vocabulary-estimation.pair-frequency-logistic-v2-test]))
@@ -9,6 +10,7 @@
(let [{:keys [fail error]}
(test/run-tests
'language-learning.vocabulary-estimation.article-equation-code-test
+ 'language-learning.vocabulary-estimation.bayes-theorem-simulations-test
'language-learning.vocabulary-estimation.pair-frequency-logistic-v2-test
'language-learning.vocabulary-estimation.pair-frequency-logistic-v2-fixture-test
'language-learning.vocabulary-estimation.pair-frequency-logistic-v2-gate-test)]