morphex 0.0.0-stage → 1.0.0

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.
package/BENCHMARKS.md ADDED
@@ -0,0 +1,95 @@
1
+ # Benchmarks and bundle sizes
2
+
3
+ These are **Node.js/V8 micro-benchmarks** for the TypeScript port. They are measured with a
4
+ different runtime and harness from the Java JMH results on the main site and are not comparable
5
+ with them. Nothing here is a cross-language speed claim.
6
+
7
+ ## Method
8
+
9
+ - Harness: `morphex-ts/scripts/bench.ts` (`npm run bench`), no external benchmark dependency.
10
+ - Machine used for the recorded run: 12th Gen Intel Core i5-12500H, Windows 11 (10.0.26300).
11
+ - Runtime: Node.js 24.20.0 (the package targets Node 18+).
12
+ - Each workload warms up for one full pass, then takes seven timed samples and reports the
13
+ median. The first timed sample is discarded while V8 finishes optimizing.
14
+ - Repeated workloads run 1,000,000 calls. Distinct-word workloads run ten passes over 4,000
15
+ unique lemmas (40,000 calls per timed sample) taken from the committed P1 fixture.
16
+ - Five runs were recorded on the same machine to show the spread; ranges below are the
17
+ minimum and maximum medians, not a confidence interval.
18
+ - Provider construction was measured separately with a tsx harness on the same machine: one
19
+ warm-up, then twelve samples per run, with the median of each of two runs reported.
20
+
21
+ Reproduce with:
22
+
23
+ ```bash
24
+ cd morphex-ts
25
+ npm install
26
+ npm run build:data # writes the generated lexicons and dist/sizes.json
27
+ npm run bench # writes bench-results.json and prints the table
28
+ ```
29
+
30
+ ## Latency (ns/op, lower is better)
31
+
32
+ | Workload | Delta engine | Full engine | Rules-only |
33
+ | --- | ---: | ---: | ---: |
34
+ | Repeated lexical word (`child`) | 111–119 | 269–280 | 130–139 |
35
+ | Repeated rule word (`cat`) | 112–118 | 174–188 | 104–116 |
36
+ | Repeated compound (`mother-in-law`) | 258–289 | 434–457 | 358–378 |
37
+ | Distinct fixture lemmas (4,000 words) | 354–432 | 1,006–1,057 | 327–355 |
38
+ | `analyze()` repeated (`die`) | 258–278 | 421–441 | 221–228 |
39
+
40
+ Reading the table:
41
+
42
+ - The **delta** engine answers `pluralize()` from the shipped artifact. At load time it parses
43
+ the four artifact sections and inserts them directly into an open-addressed hash index over
44
+ the 124,883 keys, so a rule-only word such as `cat` costs one hash and a short probe before
45
+ the rules run. The original four-section implementation measured 673–678 ns; a merged-sort
46
+ binary search measured 286–293 ns; the hash index measures 112–118 ns. That is about 5.7–6.0×
47
+ faster than the original and 2.4–2.6× faster than the merged binary search, in the same
48
+ harness.
49
+ - The **full** engine is opt-in (`morphex/full`) and adds alternatives, ambiguity and
50
+ countability. It performs one binary search over its own 147,338-key array, so it is now slower
51
+ than the delta engine on these paths (182–189 ns for `cat`), but it carries the metadata the
52
+ diagnostics need.
53
+ - The **rules-only** engine (`morphex/rules`, or `withoutBundledLexicon()`) uses no data files and
54
+ loses irregular forms by design.
55
+
56
+ The hash index costs 1 MiB of slot storage (262,144 × 4 bytes) and is built once, when the
57
+ provider is created (lazily, on first use). Creating the delta provider now measures 50.3–50.8
58
+ ms median over two runs; parsing the artifact accounts for 32.0–33.1 ms and building the index
59
+ roughly 17–19 ms. Before these changes, construction measured 77–81 ms and parsing 58–65 ms.
60
+ The reduction comes from inserting each section directly into the table instead of merging the
61
+ four lists first, dropping the cross-section duplicate `Set` and the intermediate body copy,
62
+ and sharing one frozen empty alternatives array. The index is still the smaller half of
63
+ construction; nothing is computed at module import.
64
+ - Distinct-word timings include cache misses over a 4,000-word working set and are naturally
65
+ slower than repeated lookups; the spread between runs is largest there.
66
+
67
+ ## Artifact and bundle sizes
68
+
69
+ Measured by `scripts/build-lexicon.ts` and `scripts/build-site.ts` and written to
70
+ `dist/sizes.json` / `dist/site/sizes.json`.
71
+
72
+ | Artifact | Raw | Gzip |
73
+ | --- | ---: | ---: |
74
+ | Source lexicon TSV (shared with Java, not shipped) | 7,553,688 B | — |
75
+ | Delta artifact for `pluralize()` (124,883 entries) | 1,962,900 B | 562,893 B |
76
+ | Full artifact for `analyze()` (147,338 entries) | 3,707,556 B | 785,131 B |
77
+ | Membership sidecar (340,726 hashes, base64) | 1,817,208 B | 1,333,152 B |
78
+ | Browser demo bundle (full lexicon, minified ESM) | 3,735,241 B | 794,191 B |
79
+
80
+ The delta artifact is 26.0% of the source TSV's size and 84.8% of the full artifact's entries:
81
+ 85% of the lexicon is already reproduced by the three productive rules plus the identity lists,
82
+ so only the differing entries and the mass/plural-only/invariant key lists are stored.
83
+
84
+ The published `dist/lexicon-delta.txt` is the same 1,962,900-byte artifact the site links beside
85
+ the demo; browsers download it gzipped at 562,893 B.
86
+
87
+ ## Notes
88
+
89
+ - The browser demo loads the **full lexicon without the membership sidecar**: 794,191 B gzip
90
+ including the engine. Membership is only needed for unknown-uppercase initialism inference, so
91
+ the demo behaves like a rules-only build for that one policy and says so on the page.
92
+ - The `morphex/rules` entry point replaces the generated delta module at build time; it contains
93
+ zero lexicon data and therefore no CC BY-SA 3.0 material.
94
+ - Generated data and bundles live under `morphex-ts/dist/` and are gitignored; only the sizes in
95
+ this file and on the site are committed.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Baker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The lexical data bundled with this package is not covered by this MIT licence.
24
+ It is licensed under CC BY-SA 3.0; see NOTICE in this directory and the
25
+ repository's data/LICENSE-DATA.md.
package/NOTICE ADDED
@@ -0,0 +1,23 @@
1
+ morphex-ts — TypeScript port of morphex
2
+ ======================================
3
+
4
+ Code: MIT (see LICENSE in this directory).
5
+ Lexical data: CC BY-SA 3.0.
6
+
7
+ The generated lexicon artifacts (dist/lexicon-delta.txt, dist/lexicon-full.txt, the membership
8
+ table, and the browser demo bundle that embeds the full lexicon) are derived from the same
9
+ permitted corpus as the Java library's resources:
10
+
11
+ inflector-core/src/main/resources/io/github/slayyyyer/morphex/inflector/lexicon-v2.tsv
12
+ inflector-core/src/main/resources/io/github/slayyyyer/morphex/inflector/lexicon-membership-v2.bin
13
+
14
+ That data is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported licence
15
+ (CC BY-SA 3.0): https://creativecommons.org/licenses/by-sa/3.0/
16
+
17
+ Attribution: Wiktionary contributors, via Kaikki.org / Wiktextract.
18
+
19
+ The authoritative attribution and data-licence statements for the whole repository are the root
20
+ NOTICE and data/LICENSE-DATA.md; this file is a pointer, not a replacement. If you redistribute a
21
+ build of this package, keep this NOTICE, the root attribution, and the CC BY-SA 3.0 terms with it.
22
+
23
+ The `morphex/rules` entry point contains no lexicon data and therefore no CC BY-SA 3.0 material.
package/README.md CHANGED
@@ -1,3 +1,198 @@
1
- # Temporary Holding Version
1
+ # morphex (TypeScript)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Deterministic English noun pluralization for TypeScript and JavaScript. This is a native port of
4
+ the Java [morphex](../README.md) library: same precedence pipeline, same lexicon, same compound
5
+ table. No runtime dependencies, no network calls, no model downloads.
6
+
7
+ ```ts
8
+ import { EnglishInflector } from 'morphex';
9
+
10
+ const inflector = EnglishInflector.standard();
11
+
12
+ inflector.pluralize('child'); // 'children'
13
+ inflector.pluralize('CHILD'); // 'CHILDREN'
14
+ inflector.pluralize('UFO'); // 'UFOs'
15
+ inflector.pluralize('mother-in-law'); // 'mothers-in-law'
16
+ inflector.pluralize('cat', 1); // 'cat' (count === 1 keeps the source)
17
+ inflector.pluralize('cat', 3); // 'cats'
18
+
19
+ const result = inflector.analyze('die');
20
+ result.plural; // 'dice'
21
+ result.alternatives; // ['dies']
22
+ result.ambiguous; // true
23
+ result.stage; // 'LEXICON'
24
+ result.source; // 'lex:die'
25
+ result.countability; // 'COUNTABLE'
26
+ ```
27
+
28
+ Node 18+ and browsers. Zero runtime dependencies. Immutable instances: configuration is fixed at
29
+ build time, so an instance can be shared freely.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install morphex
35
+ ```
36
+
37
+ To build from a checkout instead: `cd morphex-ts && npm install && npm run build`.
38
+
39
+ Entry points:
40
+
41
+ | Import | Lexicon | Use |
42
+ | --------------- | ---------------------------- | ----------------------------------------------------------------------------- |
43
+ | `morphex` | 124,883-entry delta artifact | `pluralize()`; `analyze()` reports rule/compound stages without full metadata |
44
+ | `morphex/full` | 147,338-entry full artifact | Java-identical `analyze()`: alternatives, ambiguity, countability |
45
+ | `morphex/rules` | none | rules-only build with no data files and no CC BY-SA material |
46
+
47
+ The full artifact is opt-in so the common case does not pay for diagnostics it never reads:
48
+
49
+ ```ts
50
+ import { EnglishInflector, createBundledFullLexicon } from 'morphex/full';
51
+
52
+ const diagnostics = EnglishInflector.builder().withLexicon(createBundledFullLexicon()).build();
53
+
54
+ diagnostics.analyze('die').alternatives; // ['dies']
55
+ ```
56
+
57
+ ## API
58
+
59
+ ### `pluralize(word, count?)`
60
+
61
+ `pluralize(word, 1)` returns the input unchanged, including its original casing. Any other count
62
+ pluralizes. Both are pure functions of the configured instance.
63
+
64
+ ### `analyze(word): Inflection`
65
+
66
+ Every field is always present; nothing is optional.
67
+
68
+ ```ts
69
+ interface Inflection {
70
+ readonly input: string;
71
+ readonly plural: string;
72
+ readonly alternatives: readonly string[];
73
+ readonly ambiguous: boolean;
74
+ readonly stage: 'OVERRIDE' | 'LEXICON' | 'INFERRED' | 'COMPOUND' | 'RULE' | 'FALLBACK';
75
+ readonly source: string; // stable id, e.g. 'lex:child', 'rule:default-s'
76
+ readonly countability: 'COUNTABLE' | 'MASS' | 'MIXED' | 'PLURAL_ONLY' | 'UNKNOWN';
77
+ readonly invariant: boolean;
78
+ readonly note: string;
79
+ readonly diagnostics: boolean; // false when the provider cannot supply full metadata
80
+ isAccepted(form: string): boolean;
81
+ }
82
+ ```
83
+
84
+ `stage` follows the Java `DecisionSource`; `source` is the Java `Inflection.id()`.
85
+
86
+ ### Configuration
87
+
88
+ ```ts
89
+ const custom = EnglishInflector.builder()
90
+ .withOverride('goose', 'geese') // exact, case-sensitive whole-input match
91
+ .withOverride('API', 'APIs')
92
+ .withInitialism('ABC') // suffix policy for a declared acronym
93
+ .withMode('CLASSICAL') // prefer attested classical forms
94
+ .withCasePolicy('LOWERCASE') // render the whole result in one casing
95
+ .withoutBundledLexicon() // rules-only: no data files at all
96
+ .build();
97
+ ```
98
+
99
+ Overrides win over everything else and are returned literally. The builder is fluent and typed;
100
+ `build()` returns an `EnglishInflector`. Instances are immutable.
101
+
102
+ ### Custom lexicons
103
+
104
+ Every data source implements the same provider interface, so bundled, opt-in, rules-only and
105
+ caller-supplied data are interchangeable:
106
+
107
+ ```ts
108
+ import { EnglishInflector, createLexicon } from 'morphex';
109
+
110
+ const provider = createLexicon([
111
+ { lemma: 'cactus', plural: 'cactuses', classical: 'cacti', alternatives: ['cacti'] },
112
+ ]);
113
+
114
+ const inflector = EnglishInflector.builder().withLexicon(provider).build();
115
+ ```
116
+
117
+ `createFullLexicon`, `createDeltaLexicon` and `RULES_ONLY_LEXICON` are exported for the same
118
+ purpose. A malformed artifact, duplicate key, unsorted file or unknown flag throws at construction
119
+ time; the loaders never guess.
120
+
121
+ ## Precedence
122
+
123
+ First match wins, exactly as in the Java library:
124
+
125
+ 1. **Caller override** — exact, case-sensitive whole-input match.
126
+ 2. **Exact lexicon entry** — case-insensitive lookup, with case-sensitive entries where needed.
127
+ 3. **Declared initialism** — configured acronym suffix policy.
128
+ 4. **Supported compound** — bounded table: exact entries, free tails and curated postposed heads.
129
+ Unmatched input is left alone, not guessed.
130
+ 5. **Three productive rules** — `+es` after sibilants, consonant + `y` → `ies`, default `+s`.
131
+ 6. **Documented fallback** — unsupported shapes are returned unchanged and reported by `analyze`.
132
+
133
+ Lookup normalisation is separate from output case restoration, so `CHILD → CHILDREN` and
134
+ `UFO → UFOs` are both correct.
135
+
136
+ ## Rules-only vs bundled lexicon
137
+
138
+ | Build | Data shipped | `pluralize('child')` | `analyze()` metadata |
139
+ | --------------- | -------------------------------: | --------------------- | ----------------------------------------------- |
140
+ | `morphex/rules` | 0 B | `childs` (rules only) | rules/compound stages, `diagnostics: false` |
141
+ | `morphex` | 1,962,900 B raw / 562,893 B gzip | `children` | rule/compound stages; partial lexical metadata |
142
+ | `morphex/full` | 3,707,556 B raw / 785,131 B gzip | `children` | complete: alternatives, ambiguity, countability |
143
+
144
+ A rules-only build is a plain `EnglishInflector.builder().withoutBundledLexicon().build()` and
145
+ needs no data files. The delta engine parses its four artifact sections and builds a 1 MiB
146
+ open-addressed hash index at construction (roughly 50 ms end to end on the measured machine,
147
+ with 17–19 ms of that building the index), so lookups do not scan the sorted keys; nothing is
148
+ built at module import. The full artifact also has a
149
+ 1,817,208 B (1,333,152 B gzip) membership sidecar used only for uppercase-initialism inference.
150
+
151
+ ## Building data
152
+
153
+ The lexicons are generated from the repository's authoritative artifact, not copied:
154
+
155
+ ```bash
156
+ npm run build:data
157
+ ```
158
+
159
+ Reads `inflector-core/src/main/resources/io/github/slayyyyer/morphex/inflector/lexicon-v2.tsv`
160
+ and `lexicon-membership-v2.bin`, and writes gitignored modules under `src/generated/` plus
161
+ `dist/lexicon-delta.txt`, `dist/lexicon-full.txt` and `dist/sizes.json`.
162
+
163
+ The delta artifact stores only entries whose answer differs from the productive rules, plus
164
+ sorted key lists for mass, plural-only and invariant nouns. Hyphenated and spaced lemmas are kept
165
+ even when the rules would reproduce them, because the rules are unreachable behind the compound
166
+ policy.
167
+
168
+ ## Conformance
169
+
170
+ Both ports are tested against fixtures exported from the Java engine:
171
+
172
+ - `test-fixtures/p1.json` — all 50,978 frozen P1 cases, tuple-typed in
173
+ `test-fixtures/schema.ts`.
174
+ - `test-fixtures/upstream-probe.json` — the `docs/upstream-probe.md` table.
175
+ - `site/catalog.json` — the verified catalog used by the Java site demo.
176
+
177
+ Vitest runs every case through both the delta and full engines and compares the plural, accepted
178
+ forms, primary flag, countability, invariant flag, stage and source id. CI regenerates the
179
+ fixtures with Java and fails on any diff, so the ports cannot drift silently.
180
+
181
+ ```bash
182
+ npm test # typecheck-friendly Vitest suite
183
+ npm run typecheck # strict tsc, no emit
184
+ npm run lint # typescript-eslint + Prettier
185
+ npm run build # ESM + CJS + .d.ts/.d.cts via tsup
186
+ npm run check:package # publint + arethetypeswrong
187
+ ```
188
+
189
+ ## Benchmarks
190
+
191
+ See [BENCHMARKS.md](BENCHMARKS.md) for measured Node timings and bundle sizes. The numbers are
192
+ Node/V8 micro-benchmarks and are deliberately separate from the Java JMH results.
193
+
194
+ ## Licence
195
+
196
+ Code: MIT (see `LICENSE`). Bundled lexicon data: CC BY-SA 3.0, Wiktionary contributors via
197
+ Kaikki.org / Wiktextract — see `NOTICE` and the repository's root `NOTICE` and
198
+ `data/LICENSE-DATA.md`. The `morphex/rules` entry point contains no lexicon data.