@willwade/aac-processors 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/browser/core/treeStructure.js +3 -1
  2. package/dist/browser/metrics.js +2 -0
  3. package/dist/browser/processors/astericsGridProcessor.js +21 -12
  4. package/dist/browser/processors/gridset/helpers.js +3 -3
  5. package/dist/browser/processors/gridsetProcessor.js +269 -239
  6. package/dist/browser/processors/obfProcessor.js +4 -4
  7. package/dist/browser/processors/snap/helpers.js +3 -3
  8. package/dist/browser/processors/snapProcessor.js +2 -2
  9. package/dist/browser/processors/touchchatProcessor.js +6 -6
  10. package/dist/browser/utilities/analytics/metrics/core.js +213 -8
  11. package/dist/browser/utilities/analytics/metrics/vocabulary.js +13 -1
  12. package/dist/browser/utilities/analytics/morphology/engine.js +910 -0
  13. package/dist/browser/utilities/analytics/morphology/grid3VerbsParser.js +455 -0
  14. package/dist/browser/utilities/analytics/morphology/index.js +3 -0
  15. package/dist/browser/utilities/analytics/morphology/types.js +1 -0
  16. package/dist/browser/utilities/analytics/morphology/wordFormGenerator.js +74 -0
  17. package/dist/core/treeStructure.d.ts +17 -1
  18. package/dist/core/treeStructure.js +3 -1
  19. package/dist/index.node.d.ts +2 -0
  20. package/dist/index.node.js +6 -1
  21. package/dist/metrics.d.ts +3 -0
  22. package/dist/metrics.js +5 -1
  23. package/dist/processors/astericsGridProcessor.js +21 -12
  24. package/dist/processors/excelProcessor.js +5 -1
  25. package/dist/processors/gridset/helpers.js +3 -3
  26. package/dist/processors/gridset/imageDebug.js +2 -2
  27. package/dist/processors/gridsetProcessor.js +269 -239
  28. package/dist/processors/obfProcessor.js +4 -4
  29. package/dist/processors/snap/helpers.js +3 -3
  30. package/dist/processors/snapProcessor.js +2 -2
  31. package/dist/processors/touchchatProcessor.js +6 -6
  32. package/dist/utilities/analytics/metrics/core.d.ts +14 -0
  33. package/dist/utilities/analytics/metrics/core.js +213 -8
  34. package/dist/utilities/analytics/metrics/types.d.ts +18 -3
  35. package/dist/utilities/analytics/metrics/vocabulary.d.ts +3 -0
  36. package/dist/utilities/analytics/metrics/vocabulary.js +13 -1
  37. package/dist/utilities/analytics/morphology/engine.d.ts +30 -0
  38. package/dist/utilities/analytics/morphology/engine.js +914 -0
  39. package/dist/utilities/analytics/morphology/grid3VerbsParser.d.ts +36 -0
  40. package/dist/utilities/analytics/morphology/grid3VerbsParser.js +485 -0
  41. package/dist/utilities/analytics/morphology/index.d.ts +5 -0
  42. package/dist/utilities/analytics/morphology/index.js +9 -0
  43. package/dist/utilities/analytics/morphology/types.d.ts +40 -0
  44. package/dist/utilities/analytics/morphology/types.js +2 -0
  45. package/dist/utilities/analytics/morphology/wordFormGenerator.d.ts +10 -0
  46. package/dist/utilities/analytics/morphology/wordFormGenerator.js +78 -0
  47. package/package.json +12 -11
@@ -51,7 +51,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
51
51
  return null;
52
52
  }
53
53
  }
54
- catch (err) {
54
+ catch (_err) {
55
55
  continue;
56
56
  }
57
57
  }
@@ -97,7 +97,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
97
97
  return dataUrl;
98
98
  }
99
99
  }
100
- catch (err) {
100
+ catch (_err) {
101
101
  // Continue to next path
102
102
  continue;
103
103
  }
@@ -321,7 +321,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
321
321
  return obj;
322
322
  }
323
323
  }
324
- catch (error) {
324
+ catch (_error) {
325
325
  // Log parsing errors for debugging but don't throw
326
326
  }
327
327
  return null;
@@ -728,7 +728,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
728
728
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-return
729
729
  return require('../validation/obfValidator').ObfValidator;
730
730
  }
731
- catch (error) {
731
+ catch (_error) {
732
732
  throw new Error('Validation utilities are not available in this environment.');
733
733
  }
734
734
  }
@@ -37,7 +37,7 @@ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = io_1.defa
37
37
  try {
38
38
  entries = await listDir(current.dir);
39
39
  }
40
- catch (error) {
40
+ catch (_error) {
41
41
  continue;
42
42
  }
43
43
  for (const entry of entries) {
@@ -143,7 +143,7 @@ async function openImage(dbOrFile, entryPath, fileAdapter = io_1.defaultFileAdap
143
143
  const dir = dirname(dbPath);
144
144
  await removePath(dir);
145
145
  }
146
- catch (e) {
146
+ catch (_e) {
147
147
  // Ignore cleanup errors
148
148
  }
149
149
  }
@@ -187,7 +187,7 @@ async function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter
187
187
  }
188
188
  }
189
189
  }
190
- catch (error) {
190
+ catch (_error) {
191
191
  // Silently fail if directory access fails
192
192
  }
193
193
  return results;
@@ -218,7 +218,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
218
218
  try {
219
219
  positions = JSON.parse(sg.SerializedGridPositions);
220
220
  }
221
- catch (e) {
221
+ catch (_e) {
222
222
  // Invalid JSON, skip this group
223
223
  return;
224
224
  }
@@ -439,7 +439,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
439
439
  }
440
440
  }
441
441
  }
442
- catch (e) {
442
+ catch (_e) {
443
443
  // Ignore JSON parse errors in commands
444
444
  }
445
445
  }
@@ -104,7 +104,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
104
104
  idMappings.set(mapping.numeric_id, mapping.string_id);
105
105
  });
106
106
  }
107
- catch (e) {
107
+ catch (_e) {
108
108
  // No mapping table, use numeric IDs as strings
109
109
  }
110
110
  // Load styles
@@ -122,7 +122,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
122
122
  pageStyles.set(style.id, style);
123
123
  });
124
124
  }
125
- catch (e) {
125
+ catch (_e) {
126
126
  // console.log('No styles found:', e);
127
127
  }
128
128
  // First, load all pages and get their names from resources
@@ -310,7 +310,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
310
310
  }
311
311
  });
312
312
  }
313
- catch (e) {
313
+ catch (_e) {
314
314
  // console.log('No button box cells found:', e);
315
315
  }
316
316
  // Load buttons directly linked to pages via resources
@@ -369,7 +369,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
369
369
  page.addButton(button);
370
370
  });
371
371
  }
372
- catch (e) {
372
+ catch (_e) {
373
373
  // console.log('No direct page buttons found:', e);
374
374
  }
375
375
  // Load navigation actions
@@ -416,7 +416,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
416
416
  }
417
417
  });
418
418
  }
419
- catch (e) {
419
+ catch (_e) {
420
420
  // console.log('No navigation actions found:', e);
421
421
  }
422
422
  // Try to load root ID from multiple sources in order of priority
@@ -457,7 +457,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
457
457
  tree.metadata.defaultHomePageId = rootPageId;
458
458
  }
459
459
  }
460
- catch (e) {
460
+ catch (_e) {
461
461
  // No metadata table or other error, use first page as root
462
462
  if (rootPageId) {
463
463
  tree.rootId = rootPageId;
@@ -39,6 +39,20 @@ export declare class MetricsCalculator {
39
39
  * Calculate what percentage of links to this board match semantic_id/clone_id
40
40
  */
41
41
  private calculateBoardLinkPercentages;
42
+ /**
43
+ * Quick check whether any button in the tree has a POS tag.
44
+ * Used to auto-enable smart grammar without requiring explicit opt-in.
45
+ */
46
+ private treeHasPosTags;
47
+ /**
48
+ * Expand morphological predictions from POS tags on buttons
49
+ *
50
+ * For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
51
+ * MorphologyEngine to generate inflected word forms and populate the
52
+ * button's predictions array. This is done as a pre-processing step
53
+ * before calculateWordFormMetrics assigns effort to each form.
54
+ */
55
+ private expandMorphologicalPredictions;
42
56
  /**
43
57
  * Calculate metrics for word forms (smart grammar predictions)
44
58
  *
@@ -12,6 +12,7 @@ exports.MetricsCalculator = void 0;
12
12
  const treeStructure_1 = require("../../../core/treeStructure");
13
13
  const aac_1 = require("../../../types/aac");
14
14
  const effort_1 = require("./effort");
15
+ const morphology_1 = require("../morphology");
15
16
  class MetricsCalculator {
16
17
  constructor() {
17
18
  this.locale = 'en';
@@ -83,9 +84,11 @@ class MetricsCalculator {
83
84
  });
84
85
  // Update buttons using dynamic spelling effort if applicable
85
86
  const buttons = Array.from(knownButtons.values()).sort((a, b) => a.effort - b.effort);
86
- // Calculate metrics for word forms (smart grammar predictions) if enabled
87
- // Default to true if not specified
88
- const useSmartGrammar = options.useSmartGrammar !== false;
87
+ // Expand morphological predictions from POS tags if enabled or auto-detected
88
+ const useSmartGrammar = options.useSmartGrammar === true || this.treeHasPosTags(tree);
89
+ if (useSmartGrammar) {
90
+ this.expandMorphologicalPredictions(tree, options);
91
+ }
89
92
  if (useSmartGrammar) {
90
93
  const { wordFormMetrics, replacedLabels } = this.calculateWordFormMetrics(tree, buttons, options);
91
94
  // Remove buttons that were replaced by lower-effort word forms
@@ -154,13 +157,21 @@ class MetricsCalculator {
154
157
  }) || null;
155
158
  }
156
159
  if (!spellingPage)
157
- return { spellingPage: null, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
160
+ return {
161
+ spellingPage: null,
162
+ spellingBaseEffort: 10,
163
+ spellingAvgLetterEffort: 2.5,
164
+ };
158
165
  // Calculate effort to reach this page from root
159
166
  const rootBoard = tree.rootId
160
167
  ? tree.pages[tree.rootId]
161
168
  : Object.values(tree.pages).find((p) => !p.parentId);
162
169
  if (!rootBoard)
163
- return { spellingPage, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
170
+ return {
171
+ spellingPage,
172
+ spellingBaseEffort: 10,
173
+ spellingAvgLetterEffort: 2.5,
174
+ };
164
175
  // Analyze specifically to find the lowest effort path to the spelling page
165
176
  const result = this.analyzeFrom(tree, rootBoard, setPcts, true, options);
166
177
  const spellingBaseEffort = result.visitedBoardEfforts.get(spellingPage.id) ?? 10;
@@ -574,6 +585,157 @@ class MetricsCalculator {
574
585
  boardPcts['all'] = totalLinks;
575
586
  return boardPcts;
576
587
  }
588
+ /**
589
+ * Quick check whether any button in the tree has a POS tag.
590
+ * Used to auto-enable smart grammar without requiring explicit opt-in.
591
+ */
592
+ treeHasPosTags(tree) {
593
+ for (const page of Object.values(tree.pages)) {
594
+ for (const row of page.grid) {
595
+ for (const btn of row) {
596
+ if (btn?.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
597
+ return true;
598
+ }
599
+ }
600
+ }
601
+ }
602
+ return false;
603
+ }
604
+ /**
605
+ * Expand morphological predictions from POS tags on buttons
606
+ *
607
+ * For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
608
+ * MorphologyEngine to generate inflected word forms and populate the
609
+ * button's predictions array. This is done as a pre-processing step
610
+ * before calculateWordFormMetrics assigns effort to each form.
611
+ */
612
+ expandMorphologicalPredictions(tree, options) {
613
+ const locale = options.morphologyLocale || 'en-gb';
614
+ const morph = new morphology_1.MorphologyEngine(locale);
615
+ // Words that should never be POS-inferred (function words, determiners, etc.)
616
+ const skipInference = new Set([
617
+ 'a',
618
+ 'an',
619
+ 'the',
620
+ 'to',
621
+ 'in',
622
+ 'on',
623
+ 'at',
624
+ 'of',
625
+ 'for',
626
+ 'and',
627
+ 'or',
628
+ 'but',
629
+ 'not',
630
+ 'no',
631
+ 'yes',
632
+ 'is',
633
+ 'am',
634
+ 'are',
635
+ 'was',
636
+ 'were',
637
+ 'be',
638
+ 'been',
639
+ 'being',
640
+ 'has',
641
+ 'have',
642
+ 'had',
643
+ 'do',
644
+ 'does',
645
+ 'did',
646
+ 'will',
647
+ 'would',
648
+ 'could',
649
+ 'should',
650
+ 'shall',
651
+ 'may',
652
+ 'might',
653
+ 'can',
654
+ 'must',
655
+ 'with',
656
+ 'from',
657
+ 'by',
658
+ 'up',
659
+ 'down',
660
+ 'out',
661
+ 'off',
662
+ 'over',
663
+ 'under',
664
+ 'again',
665
+ 'then',
666
+ 'than',
667
+ 'so',
668
+ 'if',
669
+ 'when',
670
+ 'where',
671
+ 'how',
672
+ 'what',
673
+ 'who',
674
+ 'which',
675
+ 'that',
676
+ 'this',
677
+ 'these',
678
+ 'those',
679
+ 'here',
680
+ 'there',
681
+ 'now',
682
+ 'very',
683
+ 'just',
684
+ 'more',
685
+ 'also',
686
+ 'too',
687
+ 'please',
688
+ 'thank',
689
+ 'hi',
690
+ 'hello',
691
+ 'bye',
692
+ 'goodbye',
693
+ 'okay',
694
+ 'oh',
695
+ 'wow',
696
+ 'sorry',
697
+ ]);
698
+ for (const page of Object.values(tree.pages)) {
699
+ for (const row of page.grid) {
700
+ for (const btn of row) {
701
+ if (!btn || !btn.label)
702
+ continue;
703
+ let pos = btn.pos;
704
+ // If no POS tag (or Unknown/Ignore), attempt POS inference.
705
+ // Many content words on topic pages lack POS tags even though
706
+ // they are clearly nouns (e.g., "bird", "tree", "cloud").
707
+ // Strategy: check irregular tables first for confident POS,
708
+ // then fall back to Noun for single-word content labels.
709
+ if (!pos || pos === 'Unknown' || pos === 'Ignore') {
710
+ const lower = btn.label.toLowerCase();
711
+ // Skip function words and multi-word labels
712
+ if (!skipInference.has(lower) && !lower.includes(' ') && lower.length > 1) {
713
+ // Check irregular tables for confident POS assignment
714
+ const inferredPOS = morph.inferPOS(lower);
715
+ if (inferredPOS) {
716
+ pos = inferredPOS;
717
+ btn.pos = inferredPOS;
718
+ }
719
+ else {
720
+ // Default to Noun for untagged content words.
721
+ // This generates plurals (e.g., bird → birds, tree → trees).
722
+ pos = 'Noun';
723
+ btn.pos = 'Noun';
724
+ }
725
+ }
726
+ }
727
+ if (!pos || pos === 'Unknown' || pos === 'Ignore')
728
+ continue;
729
+ const forms = morph.inflect(btn.label, pos);
730
+ if (forms.length > 0) {
731
+ const existing = btn.predictions || [];
732
+ const merged = new Set([...existing, ...forms]);
733
+ btn.predictions = Array.from(merged);
734
+ }
735
+ }
736
+ }
737
+ }
738
+ }
577
739
  /**
578
740
  * Calculate metrics for word forms (smart grammar predictions)
579
741
  *
@@ -596,14 +758,53 @@ class MetricsCalculator {
596
758
  // Track buttons by label to compare efforts
597
759
  const existingLabels = new Map();
598
760
  buttons.forEach((btn) => existingLabels.set(btn.label.toLowerCase(), btn));
761
+ // Build a map of POS tags from ALL tree buttons, keyed by lowercase label.
762
+ // This ensures words on BFS-unreachable pages still contribute POS data.
763
+ const treePosMap = new Map();
764
+ const treePredictionsMap = new Map();
765
+ Object.values(tree.pages).forEach((page) => {
766
+ page.grid.forEach((row) => {
767
+ row.forEach((btn) => {
768
+ if (!btn || !btn.label)
769
+ return;
770
+ const lower = btn.label.toLowerCase();
771
+ if (btn.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
772
+ treePosMap.set(lower, btn.pos);
773
+ }
774
+ if (btn.predictions && btn.predictions.length > 0) {
775
+ const existing = treePredictionsMap.get(lower);
776
+ if (!existing || btn.predictions.length > existing.length) {
777
+ treePredictionsMap.set(lower, btn.predictions);
778
+ }
779
+ }
780
+ });
781
+ });
782
+ });
783
+ // For metrics buttons that lack POS but have a tree counterpart with POS,
784
+ // propagate the POS tag so it's available in the output.
785
+ buttons.forEach((btn) => {
786
+ const lower = btn.label.toLowerCase();
787
+ if (!btn.pos || btn.pos === 'Unknown' || btn.pos === 'Ignore') {
788
+ const treePos = treePosMap.get(lower);
789
+ if (treePos)
790
+ btn.pos = treePos;
791
+ }
792
+ });
793
+ // Note: buttons on pages unreachable via BFS from the root page are
794
+ // intentionally excluded. If there is no navigation path to a page,
795
+ // those buttons are not accessible to the user and should not count
796
+ // as available vocabulary.
599
797
  // Iterate through all pages to find buttons with predictions
600
798
  Object.values(tree.pages).forEach((page) => {
601
799
  page.grid.forEach((row) => {
602
800
  row.forEach((btn) => {
603
801
  if (!btn || !btn.predictions || btn.predictions.length === 0)
604
802
  return;
605
- // Find the parent button's metrics
606
- const parentMetrics = buttons.find((b) => b.id === btn.id);
803
+ // Find the parent button's metrics (by id first, then by label)
804
+ let parentMetrics = buttons.find((b) => b.id === btn.id);
805
+ if (!parentMetrics && btn.label) {
806
+ parentMetrics = existingLabels.get(btn.label.toLowerCase());
807
+ }
607
808
  if (!parentMetrics)
608
809
  return;
609
810
  // Calculate effort for each word form
@@ -701,7 +902,11 @@ class MetricsCalculator {
701
902
  // If no block assigned, treat as its own block at the end (fallback)
702
903
  if (blockId === null) {
703
904
  const loop = board.grid.length + (board.grid[0]?.length || 0);
704
- return { steps: rowIndex + colIndex + 1, selections: 1, loopSteps: loop };
905
+ return {
906
+ steps: rowIndex + colIndex + 1,
907
+ selections: 1,
908
+ loopSteps: loop,
909
+ };
705
910
  }
706
911
  const blockConfig = board.scanBlocksConfig?.find((c) => c.id === blockId);
707
912
  const blockOrder = blockConfig?.order ?? blockId;
@@ -21,6 +21,7 @@ export interface ButtonMetrics {
21
21
  is_word_form?: boolean;
22
22
  parent_button_id?: string;
23
23
  parent_button_label?: string;
24
+ pos?: string;
24
25
  }
25
26
  /**
26
27
  * Board/page level analysis result
@@ -120,17 +121,31 @@ export interface MetricsOptions {
120
121
  /**
121
122
  * Whether to include smart grammar word forms in metrics
122
123
  *
123
- * When true (default): Word forms from smart grammar predictions are included
124
+ * When true: Word forms from smart grammar predictions are included
124
125
  * in the metrics. If a word exists as both a regular button and a word form,
125
126
  * the version with lower effort is used.
126
127
  *
127
128
  * When false: Smart grammar word forms are excluded from metrics. Only actual
128
129
  * buttons in the tree are analyzed.
129
130
  *
130
- * Only applicable to systems that support smart grammar (e.g., Grid 3).
131
- * Default is true.
131
+ * Auto-detected by default: if any button in the tree has a POS tag (e.g.,
132
+ * from Grid 3's Action.InsertText), smart grammar is enabled automatically.
133
+ * For non-Grid-3 formats (TD Snap, TouchChat, OBF), no buttons have POS tags,
134
+ * so smart grammar is automatically disabled with zero overhead.
135
+ *
136
+ * Set explicitly to `true` to force-enable, or `false` to force-disable.
132
137
  */
133
138
  useSmartGrammar?: boolean;
139
+ /**
140
+ * Locale for morphological inflection rules
141
+ *
142
+ * When provided, the MorphologyEngine will generate inflected word forms
143
+ * (e.g., "going", "went" for "go") based on the POS tags extracted from
144
+ * the gridset. Defaults to 'en-gb'.
145
+ *
146
+ * Only used when useSmartGrammar is true.
147
+ */
148
+ morphologyLocale?: string;
134
149
  }
135
150
  /**
136
151
  * Comparison result between two board sets
@@ -60,6 +60,9 @@ export declare class VocabularyAnalyzer {
60
60
  getWordEffort(word: string, metrics: MetricsResult): number;
61
61
  /**
62
62
  * Check if a word is in the board set
63
+ *
64
+ * Checks both direct button labels and smart grammar word forms
65
+ * (morphological inflections stored as predictions).
63
66
  */
64
67
  hasWord(word: string, metrics: MetricsResult): boolean;
65
68
  }
@@ -134,9 +134,21 @@ class VocabularyAnalyzer {
134
134
  }
135
135
  /**
136
136
  * Check if a word is in the board set
137
+ *
138
+ * Checks both direct button labels and smart grammar word forms
139
+ * (morphological inflections stored as predictions).
137
140
  */
138
141
  hasWord(word, metrics) {
139
- return metrics.buttons.some((b) => b.label.toLowerCase() === word.toLowerCase());
142
+ const lower = word.toLowerCase();
143
+ return metrics.buttons.some((b) => {
144
+ if (b.label.toLowerCase() === lower)
145
+ return true;
146
+ if (b.is_word_form && b.parent_button_label) {
147
+ if (b.label.toLowerCase() === lower)
148
+ return true;
149
+ }
150
+ return false;
151
+ });
140
152
  }
141
153
  }
142
154
  exports.VocabularyAnalyzer = VocabularyAnalyzer;
@@ -0,0 +1,30 @@
1
+ import { MorphRuleSet } from './types';
2
+ import type { Grid3VerbForms } from './grid3VerbsParser';
3
+ export declare class MorphologyEngine {
4
+ private ruleSet;
5
+ private grid3Verbs?;
6
+ private cache;
7
+ constructor(ruleSetOrLocale: string | MorphRuleSet);
8
+ static fromGrid3Verbs(verbForms: Grid3VerbForms): MorphologyEngine;
9
+ get locale(): string;
10
+ inflect(base: string, pos: string): string[];
11
+ isFormOf(word: string, base: string, pos: string): boolean;
12
+ expandVocabulary(buttons: Array<{
13
+ label: string;
14
+ pos?: string;
15
+ predictions?: string[];
16
+ }>): Map<string, string[]>;
17
+ inflectWithSlots(base: string, pos: string): Array<{
18
+ slot: string;
19
+ form: string;
20
+ }>;
21
+ private computeForms;
22
+ private applyRules;
23
+ /**
24
+ * Infer the most likely POS for a word by checking the irregular tables.
25
+ * Returns the POS if found in any irregular table, or null if not found.
26
+ * Priority: Verb > Noun > Adjective > Pronoun
27
+ */
28
+ inferPOS(word: string): string | null;
29
+ private loadBundled;
30
+ }