@tagalingo/grammar-engine
v0.1.4
Published
Tagalog grammar engine for conjugation, decomposition, and drill generation
Maintainers
Readme
@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
npm install @tagolingo/grammar-engineUsage
Conjugate a verb
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
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 verbDecompose a conjugated form
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
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
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.
{ "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}.
{
"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-- formang-/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)
{ "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-- triggersni-toin-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")
{ "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
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.tsDevelopment
npm install
npm run build # tsup -> dist/ (CJS + ESM + DTS)
npm test # vitest
npm run typecheck # tsc --noEmitDictionary 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
Add your Anthropic API key to a
.envfile in the project root:ANTHROPIC_API_KEY=sk-ant-...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
# 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=2All 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 runThree 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:
- Affix Rule Lookup -- maps
(affix_class x aspect)to an affix pattern from the table - Spelling Adjustments -- reduplication, nasal substitution, um-infixing, suffix vowel harmony, vowel-initial prefix fixes
- 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:
scrape-verbs.tsfetches conjugation tables from reference sitesclassify-verbs.tsuses Claude API as a fallback for roots not found onlinevalidate-verbs.tscross-checks scraped forms against engine rules
Output is committed to verbs.json.
