npm package: @tagalingo/grammar-engine
Description: Tagalog grammar engine for conjugation, decomposition, and drill generation
Latest version: 0.1.4

# @tagolingo/grammar-engine

A rule-based Tagalog verb engine for conjugation, decomposition, case markers, and drill generation. Pure TypeScript with bundled JSON data -- no network, no database, fully offline.

## What It Does

Given a verb root like "luto" (cook), the engine can:

- **Conjugate** it across 5 focus types and 4 aspects
- **Decompose** a conjugated form like "nagluluto" back to its root, focus, and aspect
- **Assign case markers** (ang/ng/sa/si/ni/kay) to nouns based on focus type
- **Generate drills** with a correct answer and plausible distractors

All functions are synchronous, pure, and return a `Result<T, E>` type -- check `.ok` before accessing `.value`.

## Installation

```bash
npm install @tagolingo/grammar-engine
```

## Usage

### Conjugate a verb

```ts
import { conjugate } from '@tagolingo/grammar-engine'

conjugate('luto', 'AF', 'completed')
// { ok: true, value: 'nagluto' }

conjugate('kain', 'AF', 'infinitive')
// { ok: true, value: 'kumain' }

conjugate('luto', 'IF', 'completed')
// { ok: false, error: 'FOCUS_NOT_SUPPORTED', message: 'Root "luto" does not support IF' }
```

### Get a full conjugation grid

```ts
import { conjugationGrid } from '@tagolingo/grammar-engine'

const result = conjugationGrid('luto')
// result.value.grid['AF']['completed']    -> 'nagluto'
// result.value.grid['OF']['infinitive']   -> 'lutuin'
// result.value.blocked                    -> forms that don't exist for this verb
```

### Decompose a conjugated form

```ts
import { decompose, decomposeKnown } from '@tagolingo/grammar-engine'

// "What verb is this?" -- returns candidates sorted by frequency
decompose('nagluluto')
// [{ root: 'luto', focus: 'AF', aspect: 'incompleted', affix_class: 'mag', steps: [...] }]

// When you already know the root
decomposeKnown('nagluluto', 'luto')
// { root: 'luto', focus: 'AF', aspect: 'incompleted', ... }
```

### Assign case markers

```ts
import { assignMarkers } from '@tagolingo/grammar-engine'

assignMarkers('AF', [
  { role: 'actor', noun: 'Maria', type: 'name' },
  { role: 'patient', noun: 'pagkain', type: 'common' },
])
// [
//   { noun: 'Maria', role: 'actor', marker: 'si', focused: true },
//   { noun: 'pagkain', role: 'patient', marker: 'ng', focused: false }
// ]

// Switch to Object Focus and the markers shift
assignMarkers('OF', [
  { role: 'actor', noun: 'Maria', type: 'name' },
  { role: 'patient', noun: 'pagkain', type: 'common' },
])
// Maria gets 'ni', pagkain gets 'ang'
```

### Generate a drill

```ts
import { generateDrill } from '@tagolingo/grammar-engine'

generateDrill('luto', 'AF', 'completed')
// {
//   prompt: 'What is the completed Actor Focus form of luto?',
//   correct_answer: 'nagluto',
//   distractors: ['magluto', 'nagluluto', 'magluluto']
// }
```

Distractors are chosen to be plausible: same-focus-different-aspect first, then different-focus-same-aspect, then cross-combinations.

## How Irregular Verbs Work

The engine handles irregularity through four mechanisms in `verbs.json`, applied in priority order during conjugation:

### 1. `blocked_forms` -- "This form doesn't exist"

Prevents generation of forms that aren't used in modern Tagalog. Returns a `FORM_BLOCKED` error.

```json
{ "root": "dukot", "blocked_forms": ["AF_contemplated"] }
```

### 2. `stem_overrides` -- "Just use this exact form"

Completely bypasses all rules for a specific affix-class + aspect combination. The key format is `{affix_class}_{aspect}`.

```json
{
  "root": "some_verb",
  "stem_overrides": {
    "mag_completed": "the_correct_irregular_form"
  }
}
```

### 3. `irregular_flags` -- "Apply this special rule"

Activates specific spelling transformations. Currently recognized:

- **`nasal_substitution`** -- for `mang-`/`nang-` class verbs where the prefix fuses with the root's initial consonant (p/b to m, t/d/s to n, k to ng)

```json
{ "root": "bili", "irregular_flags": ["nasal_substitution"] }
```

### 4. `root_phonology` -- "This root has a special sound pattern"

Tells the engine about phonological features that affect how standard rules apply:

- **`vowel_initial`** -- triggers `ni-` to `in-` conversion for `-in`/`-an`/`i-` classes (e.g., "niaral" becomes "inaral")
- **`consonant_cluster_initial`** -- uses cluster-aware reduplication (e.g., "trabaho" becomes "tatrabaho" instead of "tratrabaho")

```json
{ "root": "aral", "root_phonology": ["vowel_initial"] }
{ "root": "trabaho", "root_phonology": ["consonant_cluster_initial"] }
```

## Fixing Incorrect Conjugations

All fixes are edits to `src/data/verbs.json`. No code changes needed. The `decompose()` reverse index rebuilds automatically from `conjugate()`, so fixes propagate everywhere.

**The form is totally wrong and no rule can produce the right answer:**
Add a `stem_overrides` entry to hardcode the correct form.

**The form shouldn't exist at all:**
Add the `{focus}_{aspect}` key to `blocked_forms`.

**A needed spelling rule isn't firing (or the wrong one is):**
Fix `irregular_flags` or `root_phonology` on the verb entry.

**The verb is using the wrong affix class for a given focus:**
Fix the `focus_map` entry to point to the correct affix class.

After making a fix, run `npm test` to verify nothing else broke.

## Types

```ts
type FocusType = 'AF' | 'OF' | 'LF' | 'BF' | 'IF'
type Aspect = 'infinitive' | 'completed' | 'incompleted' | 'contemplated'
type SemanticRole = 'actor' | 'patient' | 'location' | 'beneficiary' | 'instrument'
type NounType = 'common' | 'name' | 'name_plural'
type MarkerSet = 'ang' | 'ng' | 'sa' | 'si' | 'ni' | 'kay' | 'sina' | 'nina' | 'kina'
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E; message: string }
```

## Project Structure

```
src/
  engine/
    conjugate.ts              # Core conjugation (affix lookup + spelling rules)
    decompose.ts              # Reverse: conjugated form -> root + focus + aspect
    conjugation-grid.ts       # Full focus x aspect table for a root
    generate-drill.ts         # Quiz generation with smart distractors
    markers.ts                # Focus-driven case marker assignment
  rules/
    spelling-rules.ts         # Reduplication, nasal substitution, um-infixing, etc.
    linker.ts                 # -ng / na linker selection
  data/
    affix-table.json          # 8 affix classes x 4 aspects -> patterns
    verbs.json                # Verb database with roots, focus maps, overrides, phonology
  types.ts                    # All shared types
  index.ts                    # Public API barrel export
scripts/
  scrape-verbs.ts             # Fetch conjugation tables from reference sites
  classify-verbs.ts           # LLM fallback for unscrapeable roots
  validate-verbs.ts           # Cross-check forms against engine rules
__tests__/
  conjugate.test.ts
  decompose.test.ts
  generate-drill.test.ts
  markers.test.ts
  data-validation.test.ts
```

## Development

```bash
npm install
npm run build       # tsup -> dist/ (CJS + ESM + DTS)
npm test            # vitest
npm run typecheck   # tsc --noEmit
```

## Dictionary Enhancement Pipeline

A one-off batch pipeline (`scripts/dictionary-enhance/`) that processes the Tagalog dictionary CSV through Claude to verify definitions, replace unsuitable example sentences, and add missing English translations.

### Setup

1. Add your Anthropic API key to a `.env` file in the project root:
   ```
   ANTHROPIC_API_KEY=sk-ant-...
   ```

2. The pipeline processes the CSV at:
   ```
   ~/Downloads/tagalog_vocab_entries_with_examples - tagalog_vocab_entries_with_examples.csv
   ```

### Scripts

| Script | Command | What it does |
|---|---|---|
| `main.ts` | `npm run dict:enhance` | Reads the first 2,000 rows of the source CSV, runs each entry through three sequential agent passes (definition check, sentence evaluation, translation), and writes the enhanced CSV to `scripts/dictionary-enhance/output/tagalog_dictionary_enhanced.csv` |
| `verify.ts` | `npm run dict:verify` | Reads the enhanced CSV, checks every entry for missing fields, and runs a quality review across all entries in batches -- flagging incorrect definitions, inappropriate sentences, and inaccurate translations. Writes all issues to `output/issues.json` |
| `fix.ts` | `npm run dict:fix` | Reads `output/issues.json` and sends each flagged entry back to Claude with the specific issue and suggestion. Applies fixes to the CSV in place. Re-translates any entries whose sentences were replaced |

### Running the full pipeline

```bash
# 1. Enhance all 2,000 entries (20-30 min, ~$1-3 in API costs)
npm run dict:enhance -- --batch-size=100 --concurrency=2

# 2. Verify quality across all entries
npm run dict:verify -- --batch-size=100 --concurrency=2

# 3. Auto-fix flagged issues
npm run dict:fix

# 4. Re-verify to confirm issues are resolved
npm run dict:verify -- --batch-size=100 --concurrency=2
```

All three scripts accept `--batch-size=N` and `--concurrency=N` flags. `dict:enhance` also accepts `--limit=N` to process fewer rows (useful for testing).

### Output

```
scripts/dictionary-enhance/output/
  tagalog_dictionary_enhanced.csv   # Enhanced CSV (Definition, Example_Sentence,
                                    # Example_Translation filled in for all rows)
  issues.json                       # Issues found by verify.ts
  run-log.json                      # Per-batch stats from the enhance run
```

Three provenance columns are added to the CSV:

| Column | What it contains |
|---|---|
| `Enhanced_By` | Comma-separated list of passes that modified this entry: `definition`, `sentence`, `translation`, `fix` |
| `Original_Definition` | The original definition, preserved when it was changed |
| `Original_Example` | The original example sentence, preserved when it was replaced |

## Engine Pipeline

The conjugation engine applies three layers in order:

1. **Affix Rule Lookup** -- maps `(affix_class x aspect)` to an affix pattern from the table
2. **Spelling Adjustments** -- reduplication, nasal substitution, um-infixing, suffix vowel harmony, vowel-initial prefix fixes
3. **Sentence Framing** (future) -- template-based sentence generation with slot fillers and linker rules

### Affix Classes

| Class | Typical Focus | Example (luto) |
|---|---|---|
| `mag` | Actor | magluto, nagluto, nagluluto, magluluto |
| `um` | Actor | kumain, kumain, kumakain, kakain |
| `ma` (stative) | Actor | matulog, natulog, natutulog, matutulog |
| `maka` (potentive) | Actor | makakita, nakakita, nakakikita, makakikita |
| `mang` (distributive) | Actor | mamili, namili, namamili, mamamili |
| `in` | Object | lutuin, niluto, niluluto, lutuin |
| `i` | Benefactive/Instrumental | iluto, iniluto, iniluluto, iluluto |
| `an` | Locative | lutuan, nilutuan, nilulutuan, lulutuan |

### Data Pipeline

Verb data is populated at build time:

1. `scrape-verbs.ts` fetches conjugation tables from reference sites
2. `classify-verbs.ts` uses Claude API as a fallback for roots not found online
3. `validate-verbs.ts` cross-checks scraped forms against engine rules

Output is committed to `verbs.json`.