# Neural Forge — Content-Delivery Framework

> How content is served, executed, and personalized. The contract every module page implements.

This document is the architecture spec. It locks four decisions so the 16 module pages stay consistent and the user experience is coherent.

---

## 1. The four decisions

| Decision | Choice |
|---|---|
| **Authoring substrate** | Hand-authored static HTML + KaTeX, no build step. Pages are first-class artifacts the student can also `View Source` on. The full page contract (load order, shared scripts, KaTeX trio) lives in §6; this row is the one-line summary. |
| **Code runtime** | Tiered — pick the right tool per snippet. (See §3.) |
| **Language support** | Every code lab ships a multi-language slider. The student's choice persists. (See §4.) |
| **Personalization** | An embedded **AI Tutor** sidebar on every page, plus the existing "flavor" system already in `index.html`. (See §5.) |

No framework lock-in. No npm. A student who clones the repo to a USB stick on an airplane can still take the course offline (minus the AI tutor and hosted runtimes).

---

## 2. Page types

There are exactly five page types in Neural Forge. Knowing the type tells you the contract.

```
┌─────────────────────────────────────────────────────────────┐
│  HUB             index.html                                 │
│   ├── SETUP     setup.html                                  │
│   ├── MODULE    module-NN-<slug>.html  (×16)                │
│   │     └── TOOL     tools/<unlock>.html  (×16)             │
│   └── REFERENCE  appendix/*.html  (linalg, calc, prob, …)   │
└─────────────────────────────────────────────────────────────┘
```

| Page | Responsibilities |
|---|---|
| **Hub** (`index.html`) | Landing, progress, flavor + language picker, Forge Key wallet, links to setup, modules, tools |
| **Setup** (`setup.html`) | Install local IDE tooling for every supported language. OS-aware tabs. Copy-paste commands and a verification snippet per language. |
| **Module** (`module-NN-*.html`) | The actual lesson. Five fixed sections: Hook → Math → Code Lab → Boss Fight → Forge Gate. |
| **Tool** (`tools/*.html`) | A self-contained interactive instrument unlocked by completing a module. |
| **Reference** (`appendix/*.html`) | Optional deep-dives — measure theory, full backprop derivation, RoPE proof, etc. Linked from sidebars, never gated. |

Every page extends a common shell defined in `assets/theme.css` and pulls four shared scripts. (See §6.)

---

## 3. Code runtime — tiered

The course mixes runtimes because **no single runtime is the right answer for vectors and for training a transformer**.

| Tier | Where it runs | When we use it |
|---|---|---|
| **T0 — Pure browser** | `<script>` block, no deps | Tiny math demos, visualizations, sliders |
| **T1 — Pyodide / WebAssembly** | Pyodide for Python, `wasm` for C/Rust toy examples | First ~10 modules. Snappy, zero install. |
| **T2 — Linked external sandbox** | godbolt.org (C/CUDA), play.rust-lang.org (Rust), Replit, Triton Playground | Systems-language labs the student wants to run live without local install |
| **T3 — Student's local IDE** | Their machine, instructions in `setup.html` | The recommended path. We provide one-click "Download this lab as a .ipynb / .rs / .c" buttons. |
| **T4 — Hosted GPU runtime** | Modal / Lambda / Colab Pro link | Modules 13–16 (capstone training). Linked, not embedded. |

The module template (`_template/module.html`) declares per-snippet which tier is used. The code-lab widget dispatches automatically.

```html
<!-- Example declaration inside a module -->
<code-lab
  title="cosine_similarity from scratch"
  langs="python,c,rust,js"
  default="python"
  runtime="auto">         <!-- python→T1, c→T2(godbolt), rust→T2, js→T0 -->
  <snippet lang="python" src="labs/m01/cosine.py"></snippet>
  <snippet lang="c"      src="labs/m01/cosine.c"></snippet>
  <snippet lang="rust"   src="labs/m01/cosine.rs"></snippet>
  <snippet lang="js"     src="labs/m01/cosine.js"></snippet>
</code-lab>
```

The widget is a tiny custom element implemented in `assets/runtime.js`. The student's preferred language (stored at `localStorage.forge.state.lang`) auto-selects on page load.

---

## 4. The language slider

Languages we commit to supporting in the slider:

```
Python · JavaScript · C · Rust · JAX · Mojo · Triton · CUDA
```

Visual: a horizontal segmented control above each code block.

```
┌────────────────────────────────────────────────────────┐
│  ► Cosine similarity from scratch                      │
│                                                        │
│  [Python][JS][C][Rust][JAX][Mojo][Triton][CUDA]        │
│   ━━━━━━                                               │
│                                                        │
│  ┌────────────────────────────────────────────────────┐│
│  │ def cosine(a, b):                                  ││
│  │     dot = sum(x*y for x, y in zip(a, b))           ││
│  │     na  = (sum(x*x for x in a)) ** 0.5             ││
│  │     nb  = (sum(x*x for x in b)) ** 0.5             ││
│  │     return dot / (na * nb)                         ││
│  └────────────────────────────────────────────────────┘│
│                                                        │
│  [▶ Run]  [⬇ Download .py]  [↗ Open in Colab]          │
└────────────────────────────────────────────────────────┘
```

**Rules**

1. Every snippet is functionally equivalent across the languages it ships in — solving the same problem with the same I/O.
2. Not every snippet ships in every language. A NumPy SVD demo doesn't need a CUDA version. Disabled tabs render as grayed-out chips with a tooltip ("Not part of this lab — try the Module 14 kernel version").
3. The "Run" button only enables for tiers T0/T1 in-browser. For T2 it becomes "Open in Godbolt"; for T3 it becomes "Download"; for T4 it becomes "Launch on Modal".
4. `Download` always works — the student can grab the file and continue locally.

A reference Python version is **always** provided when at least one other language is. So even if the student picks Rust as primary, they can sanity-check against Python.

---

## 5. The embedded AI Tutor

A floating right-edge button on every page (Hub, Setup, Module, Tool). Clicking it opens a sidebar containing a chat thread scoped to that page.

```
┌───────────────────────────┐
│  💬 Tutor                 │
│  on: Module 1 — Vectors   │
├───────────────────────────┤
│ Hey traveller. I see      │
│ you're on the cosine      │
│ similarity lab. Want me   │
│ to walk through the proof │
│ that ⟨a,b⟩ = |a||b|cosθ?  │
│                           │
│ [yes]  [no, I'm good]     │
├───────────────────────────┤
│ > _your question…_        │
└───────────────────────────┘
```

**Context the tutor receives on every turn:**

- `flavor` and `secondaryFlavor` (from `forge.state`)
- `lang` — student's primary language
- Current `page.id` (e.g., `module-01`)
- The IDs of every concept the student has marked "I'm stuck on this" via the in-line ⚠ button
- The student's recent Forge Gate failures (count + last submission, hashed)
- Last N (default 20) messages in this page's thread

**Where the LLM calls go:** A pluggable endpoint declared at `window.NeuralForge.tutor.endpoint`.
The resolution order is:

1. Whatever the page set on `window.NeuralForge.tutor.endpoint` before `assets/tutor.js` ran.
2. `localStorage.getItem('forge.tutor.endpoint')` — advanced students can pin a custom URL.
3. **Auto-detect:**
   - `file://` or `localhost`/`127.0.0.1`/`*.local` → `http://localhost:8787/tutor` (local dev proxy).
   - Any real host (e.g. `thorntonstatistical.com/nnc/…`) → resolves relative to the
     `assets/tutor.js` script URL to `../api/tutor`, which lands at `/nnc/api/tutor` on
     production and `/api/tutor` on the bare `*.vercel.app` deploy. Same code path either way.

**Production tutor implementation:** `api/tutor.js` in this repo is a Vercel serverless function
(Node 20+, ESM, no framework). It forwards the student's prompt to Anthropic's Messages API using
the `ANTHROPIC_API_KEY` env var configured in the Vercel project. The function caps thread context
at 20 messages and strips HTML before forwarding. See the top of `api/tutor.js` for the contract.

**Local dev tutor implementation:** `server/tutor-proxy.py` is the documented (not yet shipped)
local proxy for students running the course offline. Until shipped, the offline UX is the
"Tutor offline" fallback in `assets/tutor.js` — which tells the student exactly how to wire
their own. (See `course-worker-coherency.md` direction C8 for the open work.)

**Storage:** Each page's thread persists at `localStorage.forge.tutor.<pageId>` so a refresh doesn't lose context. Tutor state never blocks page rendering.

---

## 6. Shared assets

### Page contract (load order)

Every page — hub, setup, module, tool, appendix — loads a shared set of
scripts and stylesheets in a constrained order. This table is the
authoritative source of truth; the README's page diagram, the
`_template/module.html` head block, and every authored page must match it.
If you're adding a new page type, start here.

| # | Asset                                                                          | Type             | Constraint                                                                            | Required on                                                                                                                  |
|---|--------------------------------------------------------------------------------|------------------|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| 1 | KaTeX CDN trio: `katex.min.css`, `katex.min.js`, `contrib/auto-render.min.js`  | sync CSS + 2× JS `defer` | KaTeX JS bundle must be `defer`; both JS files must precede `katex-init.js`           | Math-bearing pages: modules, `_template`, math appendices. The hub, setup, and tool pages skip these.                        |
| 2 | `assets/flavor-boot.js`                                                        | **sync, no `defer`** | First *executable* script tag in `<head>`, **before** `assets/theme.css` and **before** any inline `<style>` block | **Every page** without exception                                                                                             |
| 3 | `assets/theme.css`                                                             | sync stylesheet  | After `flavor-boot.js`                                                                | Every page that doesn't fork the shell. Today three pages still ship inline `<style>` blocks (`index.html`, `module-01-vectors.html`, `tools/matrixlab.html`) — they still need `[data-flavor=…]` overrides; see C1. |
| 4 | `assets/katex-init.js`                                                         | `defer`          | After the KaTeX trio                                                                  | Math-bearing pages (same set as #1)                                                                                          |
| 5 | `assets/runtime.js`                                                            | `defer`          | Anywhere in `<head>`                                                                  | Module + tool pages (provides `<code-lab>`, language slider)                                                                 |
| 6 | `assets/tutor.js`                                                              | `defer`          | Anywhere in `<head>`                                                                  | **Every page** (AI Tutor sidebar)                                                                                            |
| 7 | `assets/gate.js`                                                               | `defer`          | Anywhere in `<head>`                                                                  | Module pages (provides `<forge-gate>`)                                                                                       |

The one hard ordering rule is **#2 must run before any flavor-aware CSS
is parsed.** Everything else is `defer`red and sequenced by the browser
after `DOMContentLoaded`. Pages that forget #2 flash the default linguist
palette before swapping; pages that ship math without #4 either never
render KaTeX or render it twice.

```html
<!-- Required first executable script on every page. No defer. -->
<script src="<relative-path>/assets/flavor-boot.js"></script>
```

### Per-module content artifacts (not loaded by the page)

The load-order table above covers everything a **page** pulls in. A
**module** as a unit also ships content artifacts that are *not* loaded
by the module page itself — they are read by other surfaces (the hub
renderer, the spaced-repetition queue, future review widgets). A new
module author following only the load-order contract will miss them; list
them here so the per-module-content surface is complete.

| Artifact                       | Type                  | Required on             | Read by                                                                              |
|--------------------------------|-----------------------|-------------------------|--------------------------------------------------------------------------------------|
| `module-NN-<slug>.html`        | lesson page           | every module            | the student (directly via the hub's `MODULES[*].href`)                               |
| `tools/<unlock>.html`          | reward tool           | every module            | the student (via `MODULES[*].toolHref`, gated by completion)                         |
| `cards/m{NN}.json`             | spaced-repetition deck | every module            | the hub-side review queue (when shipped — coder E27); see `cards/README.md` for the JSON schema and authoring rules |

`cards/m{NN}.json` is *content*, not a script — the module page does not
`<script src>` it, the hub renderer reads it. The authoring rule (every
prove-list claim should be reinforced by at least one card) lives in
`cards/README.md`; the bidirectional consistency check is automated by
`scripts/check-cards.mjs` (run via `npm run check`).

### Directory layout

```
NNC/
├── README.md
├── CURRICULUM.md
├── PROGRESSION.md
├── FRAMEWORK.md          ← this file
├── index.html            ← hub
├── setup.html            ← IDE install tutorials
├── _template/
│   └── module.html       ← canonical module template
├── assets/
│   ├── flavor-boot.js    ← sync flavor bootstrap (FIRST script on every page)
│   ├── theme.css         ← shared CSS (flavor-aware)
│   ├── runtime.js        ← <code-lab>, language slider, runtime dispatch
│   ├── tutor.js          ← AI tutor sidebar
│   └── gate.js           ← Forge Gate logic (SHA-256 check, key minting)
├── labs/
│   └── m01/              ← per-module language-tagged source files
│       ├── cosine.py
│       ├── cosine.c
│       ├── cosine.rs
│       └── cosine.js
├── tools/
│   ├── matrixlab.html
│   ├── smoke.html            ← runtime smoke (hub Diagnostics & Recall surface, coder E37)
│   ├── cards.html            ← spaced-repetition renderer (hub Diagnostics & Recall surface, coder E27)
│   └── lint-status.html      ← author-side lint visualization (not hub-linked, coder E39)
├── cards/
│   ├── README.md         ← per-module deck convention (JSON schema)
│   └── m{NN}.json        ← per-module deck content (today only m01.json)
├── appendix/
│   └── (reference deep-dives)
├── scripts/
│   ├── verify-paths.mjs         ← lint: subpath-safe href / no localhost
│   ├── check-tails.mjs          ← lint: closing-sentinel grep (truncation gate)
│   ├── check-cards.mjs          ← lint: cards ↔ prove-list bidirectional check
│   ├── check-gate-canonical.mjs ← lint: gate-canonical agreement (FRAMEWORK §11 D10)
│   ├── check-refs.mjs           ← lint: References-convention mechanization (D49)
│   ├── check-boss-vs-gate.mjs   ← lint: Boss Fight ≠ Forge Gate (D11/D30/D50)
│   ├── check-pretest.mjs        ← lint: Pre-test convention mechanization (D6/D53)
│   ├── lint-labs.mjs            ← lint: <code-lab> authoring contract (D42, E23/E38)
│   ├── check-property-seed.mjs  ← lint: property-test sub-seed isolation (D41)
│   ├── check-claim-tags.mjs     ← lint: <code-lab> claim-sentinel set equality (D58)
│   ├── check-claim-regex.mjs    ← lint: claim-regex cross-implementation parity (D57, E40)
│   └── check-md-eof.mjs         ← lint: Markdown tail-EOF integrity (D66)
└── server/
    └── tutor-proxy.py    ← optional local proxy for the AI Tutor
```

The shared assets keep module pages thin. A module page is essentially:

```html
<!-- KaTeX (math). All three deferred; katex-init runs after both bundles parse. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script defer src="../assets/katex-init.js"></script>

<script src="../assets/flavor-boot.js"></script>            <!-- sync, BEFORE theme.css -->
<link rel="stylesheet" href="../assets/theme.css">

<script defer src="../assets/runtime.js"></script>
<script defer src="../assets/tutor.js"></script>
<script defer src="../assets/gate.js"></script>

<!-- 5 sections: hook, math, code-lab, boss-fight, forge-gate -->
<!-- Everything else is content. -->
```

A non-math page (hub, setup, tool) drops the KaTeX trio + `katex-init.js`
but keeps the rest. An appendix that ships math keeps the KaTeX block;
one that doesn't (a future plain-prose reference) can drop it.

---

## 7. State model (browser)

Everything is `localStorage` keyed under `forge.*`. No server required for progress.

```js
forge.state = {
  flavor: 'mathematician',
  secondaryFlavor: 'hacker',
  lang: 'python',
  completed: [1, 2, 3],
  keys: { 1: 'a1b2c3d4', 2: '...' },
  name: 'traveller',

  // E1/E17 — lab test-reporter state. Written by <code-lab>._recordLab in
  // assets/runtime.js whenever a lab is run. Read by the hub (labs-passed
  // stat) and by the AI Tutor (failure context).
  labs: {
    'm01-cosine': {
      status:   'passed',         // 'passed' | 'failed'
      at:       1715520000000,    // epoch ms of the most recent run
      tests:    7,                // count parsed from stdout, or null
      declared: 7,                // <code-lab tests-passed="..."> value, or null
      lang:     'python',         // language slider position at run time
      ranCount: 4,                // total Runs (passed + failed)
      error:    null              // last failure message, or null
    }
  }
}

forge.tutor.<pageId> = [ {role, content, ts}, ... ]
forge.lab.<labId>.<lang> = '<student\'s most recent edit of that snippet>'
```

A hub-side "Export progress" button serializes the whole thing to a JSON file the student can carry to another machine.

**Authoring contract for the test reporter.** A `<code-lab>` opts in to the
reporter by declaring `tests-passed="N"`. The Python or JS snippet inside
MUST print a line containing either an Arabic numeral or an English numeral
(one ... ten) immediately followed by `tests passed` (or `test passed`).
The canonical phrasing is `print("All 7 tests passed (...)")` -- anything
that matches `/(\d+|one|two|...|ten)\s+tests?\s+pass/i` will register.
Snippets that do not declare `tests-passed` are still allowed; they render
a generic `OK ran cleanly` pill on a successful run.

---

## 8. Accessibility & offline

- **A11y:** Every interactive widget is keyboard-navigable. Code blocks announce language on focus. KaTeX renders in MathML for screen readers.
- **Offline:** The course works fully offline *except* for: T2 external sandboxes, T4 GPU runtimes, and the AI Tutor (unless paired with local Ollama). A service worker (added in a later pass) will cache the CDN deps so subsequent visits are airplane-friendly.

---

## 9. What "ship a research LM" looks like end-to-end

```
Module 1   …  pure browser, Pyodide       (T0/T1)
Module 6   …  C lab via Godbolt           (T2)
Module 12  …  PyTorch local, GPU optional (T3)
Module 14  …  Triton on Colab/Modal       (T4)
Module 16  …  Modal/Lambda training run   (T4)
              + LLM judge auto-review     (Tutor API)
              + arXiv-style writeup       (export from Tool)
```

The same UI carries the student all the way. The runtime tier shifts under their feet, but the slider, tutor, gates, and progression remain identical.

---

## 10. Out of scope (deliberately)

- Server-side auth / accounts. The progression is per-browser. (A future "sync" button can export/import.)
- Video. Every lesson is text + interactive code. Videos are added later as supplementary, not core.
- Mobile. The hub is responsive; module labs require a real keyboard. We mark this clearly on the hub.

## 11. Rigor conventions

Each module ships a closing **"What you can now prove"** checklist (the `<ul class="prove-list">`).
For the dashboard, the AI Tutor, and cross-module rigor metrics to work, the claim IDs need a
shared namespace.

**Naming rule:** every claim ID is a short kebab-case slug, scoped per module. Tick state lives
at `localStorage.forge.prove.m{NN}` as `{ <slug>: true, ... }`. The hub aggregates across modules
to compute a global "rigor coverage" number (`claims_ticked / claims_authored`).

**Reserved slugs.** Recurring concepts use the *same* slug across modules so the tutor can detect
"the student ticked `cs` in M1; reuse the assumption in M6 derivation." Reserve these:

| Slug | Claim |
|---|---|
| `cs` | Cauchy–Schwarz inequality (any inner-product setting) |
| `dotproof` | $u\cdot v = \|u\|\|v\|\cos\theta$ via Law of Cosines |
| `chainrule` | Chain rule for composite differentiable maps |
| `matvec` | Row-dot interpretation of $Mv$ |
| `softmaxgrad` | $\nabla_z \text{CE}(\text{softmax}(z), y) = \text{softmax}(z) - y$ |
| `varprop` | Variance preservation under linear+ReLU layers (He/Xavier) |
| `sqrtdk` | $\text{Var}(q\cdot k) = d_k$ under iid unit-variance entries |
| `bptt` | Vanishing/exploding gradient bound for unrolled recurrences |
| `xent_mle` | Cross-entropy minimization $\equiv$ MLE of categorical likelihood |
| `kl_nonneg` | $\text{KL}(p\|q) \ge 0$ with equality iff $p = q$ |
| `zero` | Orthogonality test: $u\cdot v = 0 \iff u \perp v$ (for non-zero $u, v$) |
| `cosbounds` | $-1 \le \cos\theta \le 1$ (from Cauchy–Schwarz) |

Module-specific claims use module-prefixed slugs (`m07-adam-bias`, `m13-chinchilla`, ...).
This avoids the trap where M1 owns `cs` but M11 reinvents it as `cauchy-schwarz` — the tutor would
miss the cross-reference, and the hub's rigor coverage metric would double-count what is logically
the same theorem. When in doubt, reuse a reserved slug or add a new one to this table.

### Pre-test convention

Every module ships a **Pre-test** block immediately after the Hook (Scene 01b in the
template). It exists to fulfill the PROGRESSION.md "Mastery Loop" promise — *Pre-test → see
where you are* — and to give the AI tutor a signal for where to calibrate depth.

**Authoring rules:**
- **Exactly three questions.** Two recall, one "edge of knowledge."  Three trivial
  questions teach nothing; three impossible ones discourage.  Mix.
- **Single canonical-form answer.** Integer, single word, lowercase yes/no.  The grader
  is `input.value.trim().toLowerCase() === data-pq-answer.trim().toLowerCase()`.
- **Optional synonyms (D43).** When the canonical answer has well-known synonyms
  (e.g. `orthogonal` ↔ `perpendicular` ↔ `normal`), ship an optional
  `data-pq-answer-alts="alt1,alt2,..."` attribute alongside `data-pq-answer`.
  Each alt is comma-separated and trim+lowercase-normalized at grade time; the
  grader accepts the canonical answer OR any alt.  This prevents false-negative
  signal — a student who types a textbook synonym should not be classified as
  "didn't know" in the tutor's calibration data.  Omit when the question is
  genuinely single-form (an integer, a single named operator, a yes/no).
- **Hints are hidden until wrong.** Pre-revealing the hint defeats the probe.  Authoring
  hints should orient the student toward the relevant identity, not give the answer.
- **Soft-graded.** No gate, no unlock penalty.  The block records gaps; that's its job.

**Storage schema** — written by the template's pre-test JS, read by the AI tutor and any
future "where is the student starting from?" widget:

```
localStorage.forge.pretest.m{NN} = {
  q1: { answer: "<student's typed answer>",   correct: <bool>, at: <epoch ms> },
  q2: { answer: "...",                         correct: <bool>, at: <epoch ms> },
  q3: { answer: "...",                         correct: <bool>, at: <epoch ms> }
}
```

The module number `{NN}` comes from `data-module` on the `#pretest` element.  Tutor
prefetch should treat the *most recent* `at` per question as the authoritative entry
— the student may revisit and revise.

**Implementation status (D53, 2026-05-13):** Mechanized by `scripts/check-pretest.mjs` (wired into `npm run check`).  Seven per-page checks: presence of `<div id="pretest" class="forge-pretest" data-module="NN">` block; `data-module` is two-digit and matches the filename's module number; exactly three `<div data-pq="qK">` children with ids q1/q2/q3; each carries a non-empty `data-pq-answer` in canonical form (`value === value.trim().toLowerCase()`); if `data-pq-answer-alts` is present, every alt is non-empty/canonical, no alt duplicates the canonical answer, and no two alts are identical; each q-div ships a hint surface (`data-pq-hint` attribute or `<p class="pq-hint">` child) plus an `<input>` and a `.pq-status`; placeholder slots like `{{PT_Q1_ANSWER}}` fail loudly when shipped in a non-template file.  Skips `_template/`, `*-smoke.*`, `module-9N-*`, and files with the D54 `<!-- LINT-SKIP-FIXTURE -->` first-line marker.

### Property-based testing convention (sub-seed isolation)

Every Python code lab seeds its module-level RNG with `random.seed(0)` for
reproducibility; every JS code lab declares a `mulberry32(0)` general-purpose stream.
But the **property-based** block at the end of each lab (introduced in D29)
must NOT consume from that shared stream.  Reason: if a student adds a scratch test
between the seed line and the property block, the 200 trials desynchronize between
students — the "deterministic" property test becomes "deterministic conditional on
the rest of the test set being unchanged," which is half-deterministic.

**Convention:** every property-based block opens its own dedicated stream seeded with
a small per-block integer (`1` for the first property block in a lab, `2` for the
second, etc.).  Concretely:

```python
# Python
_pt_rng = random.Random(1)          # dedicated, isolated from module random
for _ in range(N):
    u = [_pt_rng.uniform(...) for _ in range(d)]
    ...
```

```javascript
// JavaScript
const propertyRand = mulberry32(1); // dedicated stream
for (let trial = 0; trial < N; trial++) {
  const u = Array.from({length: d}, () => propertyRand() * ... );
  ...
}
```

The general-purpose `random` / `rand` stays available for any non-property test that
genuinely *wants* shared state.  This separates "tests that pin specific inputs" from
"tests that assert a property over a sweep" at the seed level.

**Trial-count rule (D59, 2026-05-13):** every property-based sweep must run at least **100 trials**, with **200 preferred** (the M1 prototype's calibration).  The trial count must appear as a *literal integer at the loop site* — `for _ in range(200):` in Python, `for (let trial = 0; trial < 200; trial++)` in JS — not as a named constant referenced from elsewhere in the snippet.  The reason: a property test that runs 10 trials proves nothing about the property; statistical significance for catching Cauchy–Schwarz-class violations (rare, deterministic) sits comfortably above 100.  A named constant hides the trial count at code-review time, the same readability failure mode the sub-seed rule fixes for seed values.  Modules that go above 200 are fine; the convention is a *floor*, not a target.

**Implementation status (D56 + D60, 2026-05-13):** Mechanized by `scripts/check-property-seed.mjs` (wired into `npm run check` as the 9th lint).  Per-runnable-snippet checks across every `<code-lab>` python and js block: (A) every `random.seed(N)` literal must be `N == 0`; (B) every `random.Random(N)` literal must be an integer `>= 1`; (C) every `const rand = mulberry32(N)` must be `N == 0`; (D) every `const propertyRand[\w]* = mulberry32(N)` must be an integer `>= 1`; **(E, D60) the first statement-level `for <var> in range(M):` after `random.Random(...)` must use an integer literal `M >= 100`**; **(F, D60) the first `for (let <var> = 0; <var> < M; <var>++)` after `const propertyRand[\w]* = mulberry32(...)` must use an integer literal `M >= 100`**.  Non-literal seeds (e.g. `random.Random(seed_var)`) and non-literal trial counts (`range(N)` where `N` is a name binding) fail loudly — the convention is about *visible* seed isolation and *visible* trial count, so a variable would hide the value at code-review time.  T2/T3/T4 snippets (C, Rust, JAX, CUDA, Mojo, Triton) are skipped because the runtime never executes them in-browser.  Skips `_template/` is **not** applied here (the template is the canonical seed-isolation example and must satisfy its own contract), but `LINT-SKIP-FIXTURE`, `SKIP_FILE_RE`, and `SKIP_DIRS` are honoured per the shared `_lint-utils.mjs` convention.

### Forge Gate input normalization (D10)

Forge Gate inputs grade as `sha256(canonical(input)) === expected_hash` where
`canonical(s) = s.trim().toLowerCase().replace(/\s+/g, '')`.  Whitespace-strip is
what lets `"8,9"`, `"8, 9"`, and `"  8 , 9 "` all hash identically while `"9,8"`
and `"(8,9)"` do not.

**Authoring rules:**

- Publish the SHA-256 of the *smallest canonical form*: a single integer, comma-
  separated integers without spaces, a single lowercase word, or a single named
  operator.
- Display the canonical format adjacent to the input field — a label like
  "Comma-separated integers, no spaces" prevents a format-error from reading
  as a math-error.
- Treat any non-alphanumeric character beyond `,` `.` `-` as a publication risk;
  parentheses, braces, equals signs, and unicode dashes are not stripped.
- Never share the Forge Gate hash with the Boss Fight answer (D11/D30) — the
  Boss Fight is the harder applied check, the Forge Gate is the sanity check.

**Implementation status (rigor D48 — closed by coder run 6 + coder E31 lint, 2026-05-12):**
`assets/gate.js` (the shared `<forge-gate>` element) and the inline `sha256()`
in `module-01-vectors.html` both delegate through `canonical(s)` per the
spec above.  M1's published hash for `"8,9"` happened to already be in
canonical form, so no re-mint was required; future modules should mint
against `sha256(canonical(answer))` explicitly.  `scripts/check-gate-canonical.mjs`
(coder E31, wired into `npm run check`) now mechanizes the rigor invariant —
it extracts every `canonical(s)` body in the repo and asserts each produces
identical output to the spec across a 16-input matrix (8 whitespace/case
variants of `"8,9"` plus 8 structurally-distinct controls).  Authors who
add a new gate should still mint with the one-liner
`python3 -c "import hashlib; print(hashlib.sha256('YOUR_ANSWER'.strip().lower().replace(' ', '').encode()).hexdigest())"`
and let the lint guard against drift in the runtime implementations.

### Code lab claim coverage (D42)

Every code lab ships **exactly six tests** (run-3 E20 contract): four behavioural
(parallel · orthogonal · zero-vector · dimension-mismatch), one numerical-stability
(D5/D32), and one property-based sweep (D29/D41).  Each behavioural test should
map to a prove-list claim where the math has one — e.g., M1's "parallel" test
reinforces `dotproof`+`cosbounds`, "orthogonal" reinforces `zero`, "200-trial
sweep" reinforces `cs`.  Stability and dim-mismatch tests are framework-level
(no claim).

**Authoring rule:** the JS and Python lab snippets must reinforce the *same*
claims.  Per-language label drift (a JS test asserting `cs` while the Python
equivalent asserts `cosbounds`) silently breaks cross-language tutor calibration:
a student passing the lab in JS but failing in Python should re-encounter the
same claim, not a different one.  `scripts/lint-labs.js` (coder E23) is the
natural enforcement point when shipped.

**Claim-sentinel convention (D57, 2026-05-13):** the claim-to-test
mapping above is enforced at code-review time via in-snippet sentinel
comments.  Python snippets author `# claim: <slug>` on a line of its
own immediately above the assert that reinforces that claim; JS
snippets use `// claim: <slug>`.  Multiple claims for the same test
stack as adjacent sentinel lines (M1's parallel test reinforces both
`dotproof` and `cosbounds`, so two `# claim:` lines precede the
assert).  Framework-level tests (numerical stability, dim-mismatch,
zero-vec convention) carry **no** sentinel — they reinforce no
prove-list claim.  The cross-language invariant: the *set* of slugs
declared in the Python snippet must equal the set declared in the JS
snippet of the same `<code-lab>` block.  Set equality, not list
equality — order is irrelevant.  Each declared slug must be present
in the §11 reserved table OR module-prefixed (`m{NN}-…`); the same
naming rule the cards/prove-list family already enforces via D47.
Convention is **opt-in**: a snippet with zero `# claim:` / `// claim:`
lines passes the lint trivially.  This keeps runtime-smoke labs
(`tools/smoke.html`) — which test the runtime, not a math claim —
outside the contract while every module-lab snippet that authors
even one sentinel inherits the cross-language coupling automatically.

**Implementation status (D58, 2026-05-13):** Mechanized by
`scripts/check-claim-tags.mjs` (wired into `npm run check` as the
**10th lint**).  Per `<code-lab>` block: extracts every Python
`# claim: <slug>` sentinel and every JS `// claim: <slug>` sentinel
from the runnable-tier snippets (T0 JS, T1 Python); if either snippet
declares ≥1 sentinel, asserts set equality across the pair; validates
every declared slug against the reserved table ∪ module-prefix regex
(shared with `check-cards.mjs` D47).  Snippets with zero sentinels
are opt-out (silently skipped).  Auto-discovery and skip semantics
mirror `lint-labs.mjs` exactly (same `walk` + `findLabBlocks` shape,
same `_lint-utils.mjs` imports).

**Drift-detection footnote (coder E40, 2026-05-13):** The D57 sentinel
regex now lives in *two* places — the build-time `extractClaims()` in
`scripts/check-claim-tags.mjs` and the run-time `_parseClaims()` in
`assets/runtime.js` (the tutor handoff path).  `scripts/check-claim-regex.mjs`
(coder E40, wired into `npm run check` as the **11th lint**) mechanizes the
invariant that both regexes stay byte-identical and behavior-identical:
asserts the canonical regex source literal appears verbatim in both files,
then compiles each pair and runs them on a 10-row test matrix asserting
per-input agreement on captured slug arrays.  Mirrors the
`check-gate-canonical.mjs` (E31) pattern.  Closes the failure mode where a
refactor changes one regex but not the other — a student sees a sentinel
sail through the build but get silently dropped by the runtime tutor
handoff, or vice versa.

### Code lab minimum language coverage (E12)

Every `<code-lab>` MUST declare **at least `python` AND `js`** in its
`langs="…"` attribute and ship the corresponding `<snippet lang="python">`
and `<snippet lang="js">` children.  These are the two runnable tiers
(T1 Python via Pyodide, T0 JS native) — the only languages whose snippets
execute in-browser, write `forge.state.labs[labId]`, and surface to the
AI Tutor's lab-failure handoff (E15/E42).  Without both, a student on
either preferred language hits a tab whose Run button degrades to "open
external sandbox" and the lab-state pill never resolves to green.

**Rationale:**

- The hub stat row "Labs passed: N / M attempted" (E18) only counts
  runnable-tier passes; a JAX-only lab silently subtracts from the
  visible-progress denominator without offering any path to add to the
  numerator.
- The claim-sentinel cross-language coupling (D57) requires the same
  set of `# claim:` slugs in the Python snippet to appear as `// claim:`
  slugs in the JS snippet.  A lab with only one runnable tier cannot
  satisfy or violate the contract; the rigor signal goes silent rather
  than green or red.
- The "slide between languages — your code in each tab is saved
  separately" pedagogy in the template's lab intro implies at least two
  tabs to slide between.  One tab is a single tab.

**Authoring escape hatch (`lang-coverage-exempt`).**  A lab may opt out
by setting `lang-coverage-exempt="<reason>"` on the `<code-lab>` element,
where `<reason>` is a one-line string explaining why the minimum does
not apply (e.g., `"tier-IV demo: JAX-only intro to vmap"` for a future
M14-style lab whose whole point is a single language's idiom).  The
attribute presence with a non-empty value disables check (E) for that
block and is logged by the lint as a per-block exemption.  The reason
string is mandatory — silent opt-out is not allowed; the next reader
of the file deserves to know why this lab broke the floor.

**Implementation status (E12, 2026-05-13):** Mechanized by
`scripts/lint-labs.mjs` check (E), wired into `npm run check` via the
existing 8th-lint slot (no new lint script — the rule extends an
already-present `<code-lab>` author-contract walker).  Per-block: every
lang in `MINIMUM_LANGS = {python, js}` must appear in the parsed
`langs="…"` set after lower-casing and trimming; missing entries fail
the lint with a clear "missing minimum coverage of …" message.  The
`lang-coverage-exempt="…"` attribute (non-empty value) disables this
check for the block and is reported as `lint-labs: <file>:<line> lab
"<id>" lang-coverage-exempt: <reason>`.  Today every authored lab in
the repo (`_template/module.html`'s 8-lang prototype, `tools/smoke.html`'s
2-lang runtime smoke fixture) satisfies the contract, so the rule
landed green on first run; its purpose is to catch the *next* author
who omits one of the two from a M2-M16 lab.

### Boss Fight design (D11/D30)

The Boss Fight (Section 06 in the template) is a *strictly harder* applied
check than the Forge Gate (Section 07) — never the same problem twice.  M1's
prototype originally re-used the Forge Gate puzzle as the Boss Fight, which
let students who solved one auto-pass the other with no second moment of
insight.  Don't repeat M1's original sin.

**Authoring rules:**

- The Boss Fight should require *strictly more* of the same machinery than
  the Gate.  If the Gate is a $2\times 2$ matrix-vector product, the Boss
  Fight is a $3\times 3$ *or* a $2\times 2$ with a follow-up step
  (residual, normalization, projection, second-derived-scalar).
- The Boss Fight may have multiple right answers (e.g., a vector AND a
  scalar derived from the vector).  The Gate has exactly one canonical-form
  answer per the D10 normalization spec.
- The Boss Fight is *not gated* — there is no SHA-256 check.  It is a
  self-paced applied test the student grades against the rendered solution.
  The Gate is the hash-checked unlock.
- If the Gate is a "compute X" puzzle, the Boss Fight should be a "compute
  X *and explain why*" puzzle — the explanation is what makes it strictly
  harder, even when the arithmetic floor is the same.
- The inline note in Section 06 of `_template/module.html` documents this
  in author-comments; this §11 sub-section is the project-level contract.

**Implementation status (D50, 2026-05-13):** Mechanized by `scripts/check-boss-vs-gate.mjs` (wired into `npm run check`).  For each `module-NN-<slug>.html` at the repo root, the lint extracts the Boss Fight prompt (either a `<h2 class="forge-section"><span class="num">06</span>Boss Fight</h2>` block or a `<div class="callout">` containing `<span class="label">BOSS FIGHT…</span>`) and the Forge Gate prompt (a `<forge-gate prompt="…">` attribute, a §07 forge-section, or a `FORGE GATE` callout).  Both are normalized (HTML stripped, whitespace collapsed, lowercased) and compared.  Lint fails if either anchor is missing or the normalized prompts are identical.  Skips `_template/`, `*-smoke.*`, and `module-9N-*`.  Modules that author both sections distinctly inherit the guarantee automatically.

### Forge Gate answer design (D22)

A Forge Gate password should be derivable from the *concept* the module
teaches, not from the student's ability to multiply.  The M9 prototype's
"vocab × dim = 38597376" tests arithmetic, not understanding — a student
who plugged into a calculator passes; a student who deeply understands
embedding sizes but typo'd the multiplication fails.  Both outcomes are
wrong signals for the tutor.

**Authoring rules:**

- Prefer answers that fall out of a 1-3 line *derivation* the student must
  write down.  Examples: the cosine of the angle between two named vectors;
  the gradient of softmax-cross-entropy at a labelled point; the variance
  ratio that justifies $1/\sqrt{d_k}$.
- Avoid answers that are pure *numeric outputs* of long arithmetic chains
  (vocab × dim, batch × seq × hidden, parameter counts beyond ~5 digits).
  If the only failure mode is "I miscounted zeros," the gate is not
  testing rigor — it is testing calculator hygiene.
- When the answer *must* be numeric (because the module's whole point is
  the number — e.g., M3 SVD eigenvalues), constrain the input to 2-3
  significant figures and document the rounding rule next to the input
  field.  This collapses the calculator-hygiene failure mode.
- Cross-reference D11/D30: the Boss Fight is where the multi-step
  arithmetic belongs.  The Gate is the conceptual sanity check.
- Audit targets: M3, M6, M9, M10, M12, M14 — when these modules land,
  every gate answer should be re-evaluated against this rule before the
  hash is published.

**Implementation status (D69, 2026-05-14):** Mechanized by `scripts/check-gate-d22.mjs` (wired into `npm run check` as the 14th lint).  D22 is qualitative ("answers test concept, not arithmetic") and resists static answer inspection because the answer is hashed; the lint therefore mechanizes the *author touchpoint* rather than the answer itself.  Every Forge Gate element (either `<forge-gate ...>` template style or legacy `<input id="gate" ...>` style inside `<div class="forge-gate">`) MUST declare `data-gate-d22="..."` with one of two allowed values: **`conceptual`** (author has audited the gate against this rule and confirms the answer is conceptually derivable in 1-3 lines) or **`boss-pivot`** (author acknowledges the gate is arithmetic-heavy and cross-refs D11/D30 — multi-step arithmetic belongs in the Boss Fight; `boss-pivot` is intended as a temporary marker, with each instance a candidate for promotion to the Boss Fight side).  Lint fails on missing attribute, on out-of-set values, and reports a per-class tally on success.  M1 ships `data-gate-d22="conceptual"` on its `<input id="gate">`; the template ships `data-gate-d22="conceptual"` on its `<forge-gate>` element so every M2-M16 author inherits the declaration as a forcing function — the lint failure message quotes this sub-section directly, so the author cannot ship without consulting D22.

### References convention (D9/D39)

Every module ships a closing References section (Section 09 in the
template) with at least **three** entries spanning the
intro/standard/advanced taxonomy.  A rigorous course earns trust by
pointing students at primary sources; a hand-wavy course buries them.

**Authoring rules:**

- **Minimum three entries**, ideally six (M1 ships six).  At least one
  `intro` (a 3Blue1Brown video, a Khan Academy module, a Wikipedia
  article — something a beginner can open without intimidation), at
  least one `standard` (a textbook chapter, a foundational paper), and
  at least one `advanced` (the original paper, a numerical-analysis
  monograph, a language-spec section).
- **Every entry is hyperlinked** (D39 contract).  No bare citation
  text; the student must be able to click and read.  Prefer DOI links
  for textbooks, arXiv abstracts for papers, official YouTube
  playlists for video; vendor-blog mirrors are last-resort.
- **Primary sources preferred** over secondary explainers.  If the
  module derives the He initialization, link to He et al. 2015, not
  a Medium post about it.  Secondary explainers are fine to *supplement* a primary
  source, never to *replace* it — a student tracking the math should
  reach the source the field cites, not a paraphrase.
- **One-line pointer per entry** — annotate each `<li>` with a short
  sentence telling the student *why* they would open this reference,
  not just *what* it is.  "Chapter 3 — discusses the LoC proof Strang
  uses to motivate cosine similarity" beats "Strang Ch. 3" with no
  follow-up.
- **Level-tag markup** — every entry carries a `<span class="tag {level}">`
  where `{level}` is one of `intro`, `standard`, `advanced`.  The hub
  surfaces this taxonomy in the appendix index (see §6); the per-module
  tag keeps the level visible inside the module page itself.

**Implementation status (D49, 2026-05-13):** Mechanized by `scripts/check-refs.mjs` (wired into `npm run check`).  Five per-page checks: `<ul class="refs">` block present; ≥3 `<li>` entries; every `<li>` contains a non-empty `<a href>`; every `<li>` carries a `<span class="tag {intro|standard|advanced}">` (with class-attribute fallback); at least one entry per level present.  Auto-discovers `module-NN-<slug>.html` at the repo root; skips `_template/`, filenames containing `-smoke`, and `module-9N-*` (smoke-fixture slot).

---

*This framework is the contract. Any module that violates it should change the framework first.*
