# Word bank

The bank is the heart of the test. Current count and trajectory are tracked here.

## Current state

| | |
|---|---|
| **Words** | ~1,900 (1,741 curated + 185 imported) |
| **Questions** | ~2,500 across 14 question types |
| **Goal** | 5,000+ via continued curation + public-list imports |
| **Coverage** | K–G8 well-covered; G9–BEYOND strengthened (G9=170, G10=162, G11=128, G12=100, BEYOND=120) |
| **Sources** | See [`word-sources.md`](word-sources.md) for the full academic defence of every source we pull from |

## How it's built

Three layers stacked, each runnable independently and idempotent:

### 1. `VocabileSeeder` — the hand-crafted core (18 words)
Each word gets all 3 skills (Recognition, Recall, Production). Definitions are carefully written. Used as the gold standard for question quality.

### 2. `SinglishSeeder` — Singapore-specific (18 entries)
`kiasu`, `shiok`, `lah`, `lor`, `paiseh`, `bojio`, `chope`, `alamak`, `lobang`, etc. Each gets a Recognition question. All tagged `is_singlish=true` so the admin can hide them via the `show_singlish` config.

### 3. `BulkWordsSeeder` — the bulk pipeline (1422+ entries across 4 part-files)
The fastest way to grow the bank. Drop a new file at `database/seeders/data/bulk_words_partN.php` returning rows in this shape:

```php
['ephemeral', 'adj', 'G10', 'advanced', 'Lasting a very short time'],
//  lemma     pos    level  difficulty    definition
```

The seeder:
- Picks up every `bulk_words*.php` file via glob — no seeder code changes needed when you add more
- Upserts each word (keyed on `lemma`)
- Attaches POS via `word_pos_category` pivot (primary)
- Attaches level via `word_vocabile_level` pivot (primary)
- Sets `word_difficulty_level_id`
- Generates **one Recognition question** per word in the form *"Which word means '<definition>'?"*
- Picks 3 distractors from words **at the same difficulty tier** (so a Common-tier word never gets Rare distractors)

Re-running is safe — words and questions are upserted, never duplicated.

### 4. `GenreAssignSeeder` — back-fill genres
Tags every word with one or more of the 20 genres using:
1. `is_singlish=true` → always `singapore_singlish`
2. A curated lemma-map (~250 high-signal words, e.g. `algorithm` → `technology + academic_writing`)
3. Definition keyword scan (definitions mentioning "blood", "disease", "body" → `medical_health`)
4. Fallback to `everyday` if nothing else matched

Uses `syncWithoutDetaching` so admin overrides survive re-runs.

## To grow toward 5000

Each session adds ~600–1000 words. Plan:

| Session | Adds | Cumulative | Focus |
|---|---|---|---|
| Today | — | **1458** | K–G10 coverage |
| Next | ~800 | ~2200 | G11+, BEYOND; more domain (legal, religion, sports) |
| +1 | ~800 | ~3000 | More verbs in K–G3, more nouns G6–G9 |
| +2 | ~1000 | ~4000 | Cross-domain expansion, technical vocab |
| +3 | ~1000 | ~5000 | Final coverage gaps |

In each session: drop the next `bulk_words_partN.php`, run `php artisan db:seed --class=BulkWordsSeeder`, verify count, commit.

## Import pipeline (the path to scale)

For breadth beyond what hand-curation can reach, use the `vocab:import`
artisan command — it ingests any CSV in the documented format and tracks
provenance per row in the new `words.source` column.

```pwsh
php artisan vocab:import database/seeders/data/imports/sample_high_frequency.csv \
    --source=sample_freq --default-level=G2 --default-difficulty=common
```

A working sample CSV ships at
`database/seeders/data/imports/sample_high_frequency.csv` (~270 entries).
For production deployment, see the **Where to get the actual full lists**
section in [`word-sources.md`](word-sources.md) — three peer-reviewed
sources (NGSL, AVL, Dale-Chall 3000) together add ~5,500 net new lemmas
when imported, which alone gets the bank to the 5k+ target.

Curated rows are **protected by default** on re-import: the `source`
column stays null unless `--overwrite-source` is passed, so the
authority of hand-written items survives even when an imported list
re-introduces the same lemma.

## Quality controls

Every word goes through the admin pipeline:

- `status_id = Public` (3) at seed time — immediately served by the IRT engine
- Admin can flip any word to **Draft** to remove it from tests while editing
- Admin can edit definition, IPA, respelling, POS, levels, genres, difficulty per word at `/admin/words`
- Audit trail: `responses` table tracks every question's exposure_count + p-value, so admins can spot bad items

## See also

- [docs/irt-and-test-types.md](irt-and-test-types.md) — how the IRT engine consumes these questions
- [docs/admin-guide.md](admin-guide.md) — how admins edit words
- [docs/developer-guide.md](developer-guide.md) — how to add a new seeder batch
