opengrammar-server 2.0.615350
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/README.npm.md +95 -0
- package/bin/opengrammar-server.js +111 -0
- package/dist/server.js +48639 -0
- package/package.json +80 -0
- package/server-node.ts +159 -0
- package/server.ts +15 -0
- package/src/analyzer.ts +542 -0
- package/src/dictionary.ts +1973 -0
- package/src/index.ts +978 -0
- package/src/nlp/nlp-engine.ts +17 -0
- package/src/nlp/tone-analyzer.ts +269 -0
- package/src/rephraser.ts +146 -0
- package/src/rules/categories/academic-writing.ts +182 -0
- package/src/rules/categories/adjectives-adverbs.ts +152 -0
- package/src/rules/categories/articles.ts +160 -0
- package/src/rules/categories/business-writing.ts +250 -0
- package/src/rules/categories/capitalization.ts +79 -0
- package/src/rules/categories/clarity.ts +117 -0
- package/src/rules/categories/common-errors.ts +601 -0
- package/src/rules/categories/confused-words.ts +219 -0
- package/src/rules/categories/conjunctions.ts +176 -0
- package/src/rules/categories/dangling-modifiers.ts +123 -0
- package/src/rules/categories/formality.ts +274 -0
- package/src/rules/categories/formatting-idioms.ts +323 -0
- package/src/rules/categories/gerund-infinitive.ts +274 -0
- package/src/rules/categories/grammar-advanced.ts +294 -0
- package/src/rules/categories/grammar.ts +286 -0
- package/src/rules/categories/inclusive-language.ts +280 -0
- package/src/rules/categories/nouns-pronouns.ts +233 -0
- package/src/rules/categories/prepositions-extended.ts +217 -0
- package/src/rules/categories/prepositions.ts +159 -0
- package/src/rules/categories/punctuation.ts +347 -0
- package/src/rules/categories/quantity-agreement.ts +200 -0
- package/src/rules/categories/readability.ts +293 -0
- package/src/rules/categories/sentence-structure.ts +100 -0
- package/src/rules/categories/spelling-advanced.ts +164 -0
- package/src/rules/categories/spelling.ts +119 -0
- package/src/rules/categories/style-tone.ts +511 -0
- package/src/rules/categories/style.ts +78 -0
- package/src/rules/categories/subject-verb-agreement.ts +201 -0
- package/src/rules/categories/tone-rules.ts +206 -0
- package/src/rules/categories/verb-tense.ts +582 -0
- package/src/rules/context-filter.ts +446 -0
- package/src/rules/index.ts +96 -0
- package/src/rules/ruleset-part1-cj-pu-sp.json +657 -0
- package/src/rules/ruleset-part1-np-ad-aa-pr.json +831 -0
- package/src/rules/ruleset-part1-ss-vt.json +907 -0
- package/src/rules/ruleset-part2-cw-st-nf.json +318 -0
- package/src/rules/ruleset-part3-aw-bw-il-rd.json +161 -0
- package/src/rules/types.ts +79 -0
- package/src/shared-types.ts +152 -0
- package/src/spellchecker.ts +418 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { Issue } from '../../shared-types.js';
|
|
2
|
+
import { createRegexRule, type Rule } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export const punctuationRules: Rule[] = [
|
|
5
|
+
// ═══ Missing Question Mark (PU_QMK) ═══
|
|
6
|
+
createRegexRule({
|
|
7
|
+
id: 'PU_QMK_how_are_you',
|
|
8
|
+
category: 'grammar',
|
|
9
|
+
pattern: /\bhow\s+are\s+you\s+doing\s*$/i,
|
|
10
|
+
suggestion: 'How are you doing?',
|
|
11
|
+
reason: 'Questions should end with a question mark.',
|
|
12
|
+
}),
|
|
13
|
+
createRegexRule({
|
|
14
|
+
id: 'PU_QMK_how_do_you',
|
|
15
|
+
category: 'grammar',
|
|
16
|
+
pattern: /\bhow\s+do\s+you\b[^?]*$/i,
|
|
17
|
+
suggestion: (m) => m[0].trim() + '?',
|
|
18
|
+
reason: 'This appears to be a question. Add a question mark at the end.',
|
|
19
|
+
}),
|
|
20
|
+
createRegexRule({
|
|
21
|
+
id: 'PU_QMK_direct_question',
|
|
22
|
+
category: 'grammar',
|
|
23
|
+
pattern: /^(What|Where|When|Why|Who|Whom|Which|Whose|How)\s+\w[^?!.]*[a-zA-Z0-9]\s*$/,
|
|
24
|
+
suggestion: (m) => m[0].trim() + '?',
|
|
25
|
+
reason: 'Questions starting with a question word should end with a question mark.',
|
|
26
|
+
}),
|
|
27
|
+
createRegexRule({
|
|
28
|
+
id: 'PU_QMK_are_you',
|
|
29
|
+
category: 'grammar',
|
|
30
|
+
pattern: /^(Are|Is|Was|Were|Do|Does|Did|Have|Has|Had|Can|Could|Will|Would|Should|Shall|May|Might|Must)\s+\w[^?!.]*[a-zA-Z0-9]\s*$/,
|
|
31
|
+
suggestion: (m) => m[0].trim() + '?',
|
|
32
|
+
reason: 'Yes/no questions should end with a question mark.',
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
// ═══ Hyphenation (PU_HYP) ═══
|
|
36
|
+
createRegexRule({
|
|
37
|
+
id: 'PU_HYP_well_known',
|
|
38
|
+
category: 'grammar',
|
|
39
|
+
pattern: /\ba\s+well\s+known\s+/i,
|
|
40
|
+
suggestion: 'a well-known ',
|
|
41
|
+
reason: 'Hyphenate compound adjectives before a noun: "well-known".',
|
|
42
|
+
}),
|
|
43
|
+
createRegexRule({
|
|
44
|
+
id: 'PU_HYP_high_quality',
|
|
45
|
+
category: 'grammar',
|
|
46
|
+
pattern: /\ba\s+high\s+quality\s+/i,
|
|
47
|
+
suggestion: 'a high-quality ',
|
|
48
|
+
reason: 'Hyphenate compound adjectives before a noun: "high-quality".',
|
|
49
|
+
}),
|
|
50
|
+
createRegexRule({
|
|
51
|
+
id: 'PU_HYP_long_term',
|
|
52
|
+
category: 'grammar',
|
|
53
|
+
pattern: /\ba\s+long\s+term\s+/i,
|
|
54
|
+
suggestion: 'a long-term ',
|
|
55
|
+
reason: 'Hyphenate compound adjectives before a noun: "long-term".',
|
|
56
|
+
}),
|
|
57
|
+
createRegexRule({
|
|
58
|
+
id: 'PU_HYP_short_term',
|
|
59
|
+
category: 'grammar',
|
|
60
|
+
pattern: /\ba\s+short\s+term\s+/i,
|
|
61
|
+
suggestion: 'a short-term ',
|
|
62
|
+
reason: 'Hyphenate compound adjectives before a noun: "short-term".',
|
|
63
|
+
}),
|
|
64
|
+
createRegexRule({
|
|
65
|
+
id: 'PU_HYP_full_time',
|
|
66
|
+
category: 'grammar',
|
|
67
|
+
pattern: /\ba\s+full\s+time\s+/i,
|
|
68
|
+
suggestion: 'a full-time ',
|
|
69
|
+
reason: 'Hyphenate compound adjectives before a noun: "full-time".',
|
|
70
|
+
}),
|
|
71
|
+
createRegexRule({
|
|
72
|
+
id: 'PU_HYP_part_time',
|
|
73
|
+
category: 'grammar',
|
|
74
|
+
pattern: /\ba\s+part\s+time\s+/i,
|
|
75
|
+
suggestion: 'a part-time ',
|
|
76
|
+
reason: 'Hyphenate compound adjectives before a noun: "part-time".',
|
|
77
|
+
}),
|
|
78
|
+
createRegexRule({
|
|
79
|
+
id: 'PU_HYP_real_time',
|
|
80
|
+
category: 'grammar',
|
|
81
|
+
pattern: /\b(a|the)\s+real\s+time\s+/i,
|
|
82
|
+
suggestion: (m) => `${m[1]} real-time `,
|
|
83
|
+
reason: 'Hyphenate compound adjectives before a noun: "real-time".',
|
|
84
|
+
}),
|
|
85
|
+
createRegexRule({
|
|
86
|
+
id: 'PU_HYP_up_to_date',
|
|
87
|
+
category: 'grammar',
|
|
88
|
+
pattern: /\b(an?|the)\s+up\s+to\s+date\s+/i,
|
|
89
|
+
suggestion: (m) => `${m[1]} up-to-date `,
|
|
90
|
+
reason: 'Hyphenate compound adjectives before a noun: "up-to-date".',
|
|
91
|
+
}),
|
|
92
|
+
createRegexRule({
|
|
93
|
+
id: 'PU_HYP_state_of_art',
|
|
94
|
+
category: 'grammar',
|
|
95
|
+
pattern: /\b(a|the)\s+state\s+of\s+the\s+art\s+/i,
|
|
96
|
+
suggestion: (m) => `${m[1]} state-of-the-art `,
|
|
97
|
+
reason: 'Hyphenate compound adjectives before a noun: "state-of-the-art".',
|
|
98
|
+
}),
|
|
99
|
+
createRegexRule({
|
|
100
|
+
id: 'PU_HYP_day_to_day',
|
|
101
|
+
category: 'grammar',
|
|
102
|
+
pattern: /\b(a|the|our|my|your|his|her|their|its)\s+day\s+to\s+day\s+/i,
|
|
103
|
+
suggestion: (m) => `${m[1]} day-to-day `,
|
|
104
|
+
reason: 'Hyphenate compound adjectives before a noun: "day-to-day".',
|
|
105
|
+
}),
|
|
106
|
+
createRegexRule({
|
|
107
|
+
id: 'PU_HYP_face_to_face',
|
|
108
|
+
category: 'grammar',
|
|
109
|
+
pattern: /\b(a|the)\s+face\s+to\s+face\s+/i,
|
|
110
|
+
suggestion: (m) => `${m[1]} face-to-face `,
|
|
111
|
+
reason: 'Hyphenate compound adjectives before a noun: "face-to-face".',
|
|
112
|
+
}),
|
|
113
|
+
createRegexRule({
|
|
114
|
+
id: 'PU_HYP_one_on_one',
|
|
115
|
+
category: 'grammar',
|
|
116
|
+
pattern: /\b(a|the)\s+one\s+on\s+one\s+/i,
|
|
117
|
+
suggestion: (m) => `${m[1]} one-on-one `,
|
|
118
|
+
reason: 'Hyphenate compound adjectives before a noun: "one-on-one".',
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
// ═══ Missing Apostrophe in Contractions (PU_APO) ═══
|
|
122
|
+
createRegexRule({
|
|
123
|
+
id: 'PU_APO_dont',
|
|
124
|
+
category: 'grammar',
|
|
125
|
+
pattern: /\bdont\b/i,
|
|
126
|
+
suggestion: "don't",
|
|
127
|
+
reason: 'Missing apostrophe in contraction "don\'t".',
|
|
128
|
+
}),
|
|
129
|
+
createRegexRule({
|
|
130
|
+
id: 'PU_APO_cant',
|
|
131
|
+
category: 'grammar',
|
|
132
|
+
pattern: /\bcant\b/i,
|
|
133
|
+
suggestion: "can't",
|
|
134
|
+
reason: 'Missing apostrophe in contraction "can\'t".',
|
|
135
|
+
}),
|
|
136
|
+
createRegexRule({
|
|
137
|
+
id: 'PU_APO_wont',
|
|
138
|
+
category: 'grammar',
|
|
139
|
+
pattern: /\bwont\b/i,
|
|
140
|
+
suggestion: "won't",
|
|
141
|
+
reason: 'Missing apostrophe in contraction "won\'t".',
|
|
142
|
+
}),
|
|
143
|
+
createRegexRule({
|
|
144
|
+
id: 'PU_APO_havent',
|
|
145
|
+
category: 'grammar',
|
|
146
|
+
pattern: /\bhavent\b/i,
|
|
147
|
+
suggestion: "haven't",
|
|
148
|
+
reason: 'Missing apostrophe in contraction "haven\'t".',
|
|
149
|
+
}),
|
|
150
|
+
createRegexRule({
|
|
151
|
+
id: 'PU_APO_hasnt',
|
|
152
|
+
category: 'grammar',
|
|
153
|
+
pattern: /\bhasnt\b/i,
|
|
154
|
+
suggestion: "hasn't",
|
|
155
|
+
reason: 'Missing apostrophe in contraction "hasn\'t".',
|
|
156
|
+
}),
|
|
157
|
+
createRegexRule({
|
|
158
|
+
id: 'PU_APO_hadnt',
|
|
159
|
+
category: 'grammar',
|
|
160
|
+
pattern: /\bhadnt\b/i,
|
|
161
|
+
suggestion: "hadn't",
|
|
162
|
+
reason: 'Missing apostrophe in contraction "hadn\'t".',
|
|
163
|
+
}),
|
|
164
|
+
createRegexRule({
|
|
165
|
+
id: 'PU_APO_im',
|
|
166
|
+
category: 'grammar',
|
|
167
|
+
pattern: /\bim\b/i,
|
|
168
|
+
suggestion: "I'm",
|
|
169
|
+
reason: 'Missing apostrophe in contraction "I\'m".',
|
|
170
|
+
}),
|
|
171
|
+
createRegexRule({
|
|
172
|
+
id: 'PU_APO_ive',
|
|
173
|
+
category: 'grammar',
|
|
174
|
+
pattern: /\bive\b/i,
|
|
175
|
+
suggestion: "I've",
|
|
176
|
+
reason: 'Missing apostrophe in contraction "I\'ve".',
|
|
177
|
+
}),
|
|
178
|
+
createRegexRule({
|
|
179
|
+
id: 'PU_APO_ill',
|
|
180
|
+
category: 'grammar',
|
|
181
|
+
pattern: /\bill\b/i,
|
|
182
|
+
suggestion: "I'll",
|
|
183
|
+
reason: 'Missing apostrophe in contraction "I\'ll".',
|
|
184
|
+
}),
|
|
185
|
+
createRegexRule({
|
|
186
|
+
id: 'PU_APO_youre',
|
|
187
|
+
category: 'grammar',
|
|
188
|
+
pattern: /\byoure\b/i,
|
|
189
|
+
suggestion: "you're",
|
|
190
|
+
reason: 'Missing apostrophe in contraction "you\'re".',
|
|
191
|
+
}),
|
|
192
|
+
createRegexRule({
|
|
193
|
+
id: 'PU_APO_youve',
|
|
194
|
+
category: 'grammar',
|
|
195
|
+
pattern: /\byouve\b/i,
|
|
196
|
+
suggestion: "you've",
|
|
197
|
+
reason: 'Missing apostrophe in contraction "you\'ve".',
|
|
198
|
+
}),
|
|
199
|
+
createRegexRule({
|
|
200
|
+
id: 'PU_APO_youll',
|
|
201
|
+
category: 'grammar',
|
|
202
|
+
pattern: /\byoull\b/i,
|
|
203
|
+
suggestion: "you'll",
|
|
204
|
+
reason: 'Missing apostrophe in contraction "you\'ll".',
|
|
205
|
+
}),
|
|
206
|
+
createRegexRule({
|
|
207
|
+
id: 'PU_APO_theyve',
|
|
208
|
+
category: 'grammar',
|
|
209
|
+
pattern: /\btheyve\b/i,
|
|
210
|
+
suggestion: "they've",
|
|
211
|
+
reason: 'Missing apostrophe in contraction "they\'ve".',
|
|
212
|
+
}),
|
|
213
|
+
createRegexRule({
|
|
214
|
+
id: 'PU_APO_theyll',
|
|
215
|
+
category: 'grammar',
|
|
216
|
+
pattern: /\btheyll\b/i,
|
|
217
|
+
suggestion: "they'll",
|
|
218
|
+
reason: 'Missing apostrophe in contraction "they\'ll".',
|
|
219
|
+
}),
|
|
220
|
+
createRegexRule({
|
|
221
|
+
id: 'PU_APO_theyre',
|
|
222
|
+
category: 'grammar',
|
|
223
|
+
pattern: /\btheyre\b/i,
|
|
224
|
+
suggestion: "they're",
|
|
225
|
+
reason: 'Missing apostrophe in contraction "they\'re".',
|
|
226
|
+
}),
|
|
227
|
+
createRegexRule({
|
|
228
|
+
id: 'PU_APO_weve',
|
|
229
|
+
category: 'grammar',
|
|
230
|
+
pattern: /\bweve\b/i,
|
|
231
|
+
suggestion: "we've",
|
|
232
|
+
reason: 'Missing apostrophe in contraction "we\'ve".',
|
|
233
|
+
}),
|
|
234
|
+
createRegexRule({
|
|
235
|
+
id: 'PU_APO_well_contr',
|
|
236
|
+
category: 'grammar',
|
|
237
|
+
pattern: /\bwell\b(?=\s+(be|have|see|go|get|do|make|take|find|give|tell|come|know))/i,
|
|
238
|
+
suggestion: "we'll",
|
|
239
|
+
reason: 'Missing apostrophe in contraction "we\'ll".',
|
|
240
|
+
}),
|
|
241
|
+
createRegexRule({
|
|
242
|
+
id: 'PU_APO_were_contr',
|
|
243
|
+
category: 'grammar',
|
|
244
|
+
pattern:
|
|
245
|
+
/\bwere\b(?=\s+(going|coming|doing|making|getting|trying|looking|working|playing|running|having|being))/i,
|
|
246
|
+
suggestion: "we're",
|
|
247
|
+
reason: 'Missing apostrophe in contraction "we\'re".',
|
|
248
|
+
}),
|
|
249
|
+
createRegexRule({
|
|
250
|
+
id: 'PU_APO_hes',
|
|
251
|
+
category: 'grammar',
|
|
252
|
+
pattern: /\bhes\b/i,
|
|
253
|
+
suggestion: "he's",
|
|
254
|
+
reason: 'Missing apostrophe in contraction "he\'s".',
|
|
255
|
+
}),
|
|
256
|
+
createRegexRule({
|
|
257
|
+
id: 'PU_APO_shes',
|
|
258
|
+
category: 'grammar',
|
|
259
|
+
pattern: /\bshes\b/i,
|
|
260
|
+
suggestion: "she's",
|
|
261
|
+
reason: 'Missing apostrophe in contraction "she\'s".',
|
|
262
|
+
}),
|
|
263
|
+
createRegexRule({
|
|
264
|
+
id: 'PU_APO_thats',
|
|
265
|
+
category: 'grammar',
|
|
266
|
+
pattern: /\bthats\b/i,
|
|
267
|
+
suggestion: "that's",
|
|
268
|
+
reason: 'Missing apostrophe in contraction "that\'s".',
|
|
269
|
+
}),
|
|
270
|
+
createRegexRule({
|
|
271
|
+
id: 'PU_APO_whats',
|
|
272
|
+
category: 'grammar',
|
|
273
|
+
pattern: /\bwhats\b/i,
|
|
274
|
+
suggestion: "what's",
|
|
275
|
+
reason: 'Missing apostrophe in contraction "what\'s".',
|
|
276
|
+
}),
|
|
277
|
+
createRegexRule({
|
|
278
|
+
id: 'PU_APO_whos',
|
|
279
|
+
category: 'grammar',
|
|
280
|
+
pattern: /\bwhos\b/i,
|
|
281
|
+
suggestion: "who's",
|
|
282
|
+
reason: 'Missing apostrophe in contraction "who\'s".',
|
|
283
|
+
}),
|
|
284
|
+
|
|
285
|
+
// ═══ Exclamation Mark Overuse ═══
|
|
286
|
+
createRegexRule({
|
|
287
|
+
id: 'PU_EXC_multiple',
|
|
288
|
+
category: 'style',
|
|
289
|
+
pattern: /!{2,}/g,
|
|
290
|
+
suggestion: '!',
|
|
291
|
+
reason: 'Multiple exclamation marks are excessive. Use a single one.',
|
|
292
|
+
}),
|
|
293
|
+
|
|
294
|
+
// ═══ Spelling Pattern Rules (SP from Part 1) ═══
|
|
295
|
+
// Additional commonly misspelled words not in base spelling.ts
|
|
296
|
+
...(
|
|
297
|
+
Object.entries({
|
|
298
|
+
accomodate: 'accommodate',
|
|
299
|
+
aquire: 'acquire',
|
|
300
|
+
arguement: 'argument',
|
|
301
|
+
bizzare: 'bizarre',
|
|
302
|
+
Carribean: 'Caribbean',
|
|
303
|
+
cemetary: 'cemetery',
|
|
304
|
+
committie: 'committee',
|
|
305
|
+
concious: 'conscious',
|
|
306
|
+
consience: 'conscience',
|
|
307
|
+
dilema: 'dilemma',
|
|
308
|
+
equivilant: 'equivalent',
|
|
309
|
+
exagerate: 'exaggerate',
|
|
310
|
+
foriegn: 'foreign',
|
|
311
|
+
guarentee: 'guarantee',
|
|
312
|
+
hierarcy: 'hierarchy',
|
|
313
|
+
humourous: 'humorous',
|
|
314
|
+
indispensible: 'indispensable',
|
|
315
|
+
liason: 'liaison',
|
|
316
|
+
miscellaneous: 'miscellaneous',
|
|
317
|
+
occurrance: 'occurrence',
|
|
318
|
+
paralell: 'parallel',
|
|
319
|
+
percieve: 'perceive',
|
|
320
|
+
perserverance: 'perseverance',
|
|
321
|
+
personell: 'personnel',
|
|
322
|
+
posession: 'possession',
|
|
323
|
+
priviledge: 'privilege',
|
|
324
|
+
restarant: 'restaurant',
|
|
325
|
+
rythm: 'rhythm',
|
|
326
|
+
sargeant: 'sergeant',
|
|
327
|
+
sincerly: 'sincerely',
|
|
328
|
+
subttle: 'subtle',
|
|
329
|
+
supercede: 'supersede',
|
|
330
|
+
tendancy: 'tendency',
|
|
331
|
+
threshhold: 'threshold',
|
|
332
|
+
tommorrow: 'tomorrow',
|
|
333
|
+
unneccesary: 'unnecessary',
|
|
334
|
+
vaccuum: 'vacuum',
|
|
335
|
+
Wendsday: 'Wednesday',
|
|
336
|
+
wierd: 'weird',
|
|
337
|
+
}) as [string, string][]
|
|
338
|
+
).map(([wrong, correct]) =>
|
|
339
|
+
createRegexRule({
|
|
340
|
+
id: `SP_EXT_${wrong.toLowerCase().replace(/[^a-z]/g, '')}`,
|
|
341
|
+
category: 'spelling',
|
|
342
|
+
pattern: new RegExp(`\\b${wrong}\\b`, 'i'),
|
|
343
|
+
suggestion: correct,
|
|
344
|
+
reason: `Misspelled word. The correct spelling is "${correct}".`,
|
|
345
|
+
}),
|
|
346
|
+
),
|
|
347
|
+
];
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createRegexRule, type Rule } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════
|
|
5
|
+
* Quantity Agreement Rules — QA Module
|
|
6
|
+
* Catches: "9 year old", "2 hour meeting", "3 mile run",
|
|
7
|
+
* "5 dollar bill", missing plurals after numbers
|
|
8
|
+
* ═══════════════════════════════════════════════════════
|
|
9
|
+
*/
|
|
10
|
+
export const quantityAgreementRules: Rule[] = [
|
|
11
|
+
// ─── NUMBER + UNIT + OLD (must come first, most specific) ───
|
|
12
|
+
createRegexRule({
|
|
13
|
+
id: 'QA_NUM_year_old',
|
|
14
|
+
category: 'grammar',
|
|
15
|
+
pattern: /\b(\d+(?:\.\d+)?)\s+year[\s-]old\b/i,
|
|
16
|
+
suggestion: (m) => `${m[1]}-year-old`,
|
|
17
|
+
reason: 'Use hyphens in compound adjectives: "9-year-old".',
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
// ─── NUMBER + SINGULAR TIME UNIT (standalone/predicate) ───
|
|
21
|
+
createRegexRule({
|
|
22
|
+
id: 'QA_NUM_years_plural',
|
|
23
|
+
category: 'grammar',
|
|
24
|
+
pattern: /\b([2-9]\d*|1\d+)\s+year\b(?!\s*[-—]?\s*old)/i,
|
|
25
|
+
suggestion: (m) => `${m[1]} years`,
|
|
26
|
+
reason: 'Use the plural form "years" after numbers greater than one.',
|
|
27
|
+
}),
|
|
28
|
+
createRegexRule({
|
|
29
|
+
id: 'QA_NUM_months_plural',
|
|
30
|
+
category: 'grammar',
|
|
31
|
+
pattern: /\b([2-9]\d*|1\d+)\s+month\b/i,
|
|
32
|
+
suggestion: (m) => `${m[1]} months`,
|
|
33
|
+
reason: 'Use the plural form "months" after numbers greater than one.',
|
|
34
|
+
}),
|
|
35
|
+
createRegexRule({
|
|
36
|
+
id: 'QA_NUM_weeks_plural',
|
|
37
|
+
category: 'grammar',
|
|
38
|
+
pattern: /\b([2-9]\d*|1\d+)\s+week\b/i,
|
|
39
|
+
suggestion: (m) => `${m[1]} weeks`,
|
|
40
|
+
reason: 'Use the plural form "weeks" after numbers greater than one.',
|
|
41
|
+
}),
|
|
42
|
+
createRegexRule({
|
|
43
|
+
id: 'QA_NUM_days_plural',
|
|
44
|
+
category: 'grammar',
|
|
45
|
+
pattern: /\b([2-9]\d*|1\d+)\s+day\b/i,
|
|
46
|
+
suggestion: (m) => `${m[1]} days`,
|
|
47
|
+
reason: 'Use the plural form "days" after numbers greater than one.',
|
|
48
|
+
}),
|
|
49
|
+
createRegexRule({
|
|
50
|
+
id: 'QA_NUM_hours_plural',
|
|
51
|
+
category: 'grammar',
|
|
52
|
+
pattern: /\b([2-9]\d*|1\d+)\s+hour\b/i,
|
|
53
|
+
suggestion: (m) => `${m[1]} hours`,
|
|
54
|
+
reason: 'Use the plural form "hours" after numbers greater than one.',
|
|
55
|
+
}),
|
|
56
|
+
createRegexRule({
|
|
57
|
+
id: 'QA_NUM_minutes_plural',
|
|
58
|
+
category: 'grammar',
|
|
59
|
+
pattern: /\b([2-9]\d*|1\d+)\s+minute\b/i,
|
|
60
|
+
suggestion: (m) => `${m[1]} minutes`,
|
|
61
|
+
reason: 'Use the plural form "minutes" after numbers greater than one.',
|
|
62
|
+
}),
|
|
63
|
+
createRegexRule({
|
|
64
|
+
id: 'QA_NUM_seconds_plural',
|
|
65
|
+
category: 'grammar',
|
|
66
|
+
pattern: /\b([2-9]\d*|1\d+)\s+second\b/i,
|
|
67
|
+
suggestion: (m) => `${m[1]} seconds`,
|
|
68
|
+
reason: 'Use the plural form "seconds" after numbers greater than one.',
|
|
69
|
+
}),
|
|
70
|
+
createRegexRule({
|
|
71
|
+
id: 'QA_NUM_miles_plural',
|
|
72
|
+
category: 'grammar',
|
|
73
|
+
pattern: /\b([2-9]\d*|1\d+)\s+mile\b/i,
|
|
74
|
+
suggestion: (m) => `${m[1]} miles`,
|
|
75
|
+
reason: 'Use the plural form "miles" after numbers greater than one.',
|
|
76
|
+
}),
|
|
77
|
+
createRegexRule({
|
|
78
|
+
id: 'QA_NUM_km_plural',
|
|
79
|
+
category: 'grammar',
|
|
80
|
+
pattern: /\b([2-9]\d*|1\d+)\s+kilometer\b/i,
|
|
81
|
+
suggestion: (m) => `${m[1]} kilometers`,
|
|
82
|
+
reason: 'Use the plural form "kilometers" after numbers greater than one.',
|
|
83
|
+
}),
|
|
84
|
+
createRegexRule({
|
|
85
|
+
id: 'QA_NUM_dollars_plural',
|
|
86
|
+
category: 'grammar',
|
|
87
|
+
pattern: /\b([2-9]\d*|1\d+)\s+dollar\b/i,
|
|
88
|
+
suggestion: (m) => `${m[1]} dollars`,
|
|
89
|
+
reason: 'Use the plural form "dollars" after numbers greater than one.',
|
|
90
|
+
}),
|
|
91
|
+
createRegexRule({
|
|
92
|
+
id: 'QA_NUM_pounds_plural',
|
|
93
|
+
category: 'grammar',
|
|
94
|
+
pattern: /\b([2-9]\d*|1\d+)\s+pound\b(?!\s*(sterling|force|per))/i,
|
|
95
|
+
suggestion: (m) => `${m[1]} pounds`,
|
|
96
|
+
reason: 'Use the plural form "pounds" after numbers greater than one.',
|
|
97
|
+
}),
|
|
98
|
+
createRegexRule({
|
|
99
|
+
id: 'QA_NUM_points_plural',
|
|
100
|
+
category: 'grammar',
|
|
101
|
+
pattern: /\b([2-9]\d*|1\d+)\s+point\b/i,
|
|
102
|
+
suggestion: (m) => `${m[1]} points`,
|
|
103
|
+
reason: 'Use the plural form "points" after numbers greater than one.',
|
|
104
|
+
}),
|
|
105
|
+
createRegexRule({
|
|
106
|
+
id: 'QA_NUM_times_plural',
|
|
107
|
+
category: 'grammar',
|
|
108
|
+
pattern: /\b([2-9]\d*|1\d+)\s+time\b(?!\s+(zone|frame|stamp|out|line))/i,
|
|
109
|
+
suggestion: (m) => `${m[1]} times`,
|
|
110
|
+
reason: 'Use the plural form "times" after numbers greater than one.',
|
|
111
|
+
}),
|
|
112
|
+
createRegexRule({
|
|
113
|
+
id: 'QA_NUM_pages_plural',
|
|
114
|
+
category: 'grammar',
|
|
115
|
+
pattern: /\b([2-9]\d*|1\d+)\s+page\b/i,
|
|
116
|
+
suggestion: (m) => `${m[1]} pages`,
|
|
117
|
+
reason: 'Use the plural form "pages" after numbers greater than one.',
|
|
118
|
+
}),
|
|
119
|
+
createRegexRule({
|
|
120
|
+
id: 'QA_NUM_items_plural',
|
|
121
|
+
category: 'grammar',
|
|
122
|
+
pattern: /\b([2-9]\d*|1\d+)\s+item\b/i,
|
|
123
|
+
suggestion: (m) => `${m[1]} items`,
|
|
124
|
+
reason: 'Use the plural form "items" after numbers greater than one.',
|
|
125
|
+
}),
|
|
126
|
+
createRegexRule({
|
|
127
|
+
id: 'QA_NUM_steps_plural',
|
|
128
|
+
category: 'grammar',
|
|
129
|
+
pattern: /\b([2-9]\d*|1\d+)\s+step\b/i,
|
|
130
|
+
suggestion: (m) => `${m[1]} steps`,
|
|
131
|
+
reason: 'Use the plural form "steps" after numbers greater than one.',
|
|
132
|
+
}),
|
|
133
|
+
createRegexRule({
|
|
134
|
+
id: 'QA_NUM_words_plural',
|
|
135
|
+
category: 'grammar',
|
|
136
|
+
pattern: /\b([2-9]\d*|1\d+)\s+word\b/i,
|
|
137
|
+
suggestion: (m) => `${m[1]} words`,
|
|
138
|
+
reason: 'Use the plural form "words" after numbers greater than one.',
|
|
139
|
+
}),
|
|
140
|
+
createRegexRule({
|
|
141
|
+
id: 'QA_NUM_people_plural',
|
|
142
|
+
category: 'grammar',
|
|
143
|
+
pattern: /\b([2-9]\d*|1\d+)\s+person\b/i,
|
|
144
|
+
suggestion: (m) => `${m[1]} people`,
|
|
145
|
+
reason: 'Use "people" for counts greater than one, not "person".',
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
// ─── WRITTEN-OUT NUMBER PLURALS ───
|
|
149
|
+
createRegexRule({
|
|
150
|
+
id: 'QA_WORD_two_year',
|
|
151
|
+
category: 'grammar',
|
|
152
|
+
pattern: /\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million)\s+year\b(?!\s*[-—]?\s*old)/i,
|
|
153
|
+
suggestion: (m) => `${m[1]} years`,
|
|
154
|
+
reason: 'Use the plural form "years" after numbers greater than one.',
|
|
155
|
+
}),
|
|
156
|
+
createRegexRule({
|
|
157
|
+
id: 'QA_WORD_two_month',
|
|
158
|
+
category: 'grammar',
|
|
159
|
+
pattern: /\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million)\s+month\b/i,
|
|
160
|
+
suggestion: (m) => `${m[1]} months`,
|
|
161
|
+
reason: 'Use the plural form "months" after numbers greater than one.',
|
|
162
|
+
}),
|
|
163
|
+
createRegexRule({
|
|
164
|
+
id: 'QA_WORD_two_day',
|
|
165
|
+
category: 'grammar',
|
|
166
|
+
pattern: /\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million)\s+day\b/i,
|
|
167
|
+
suggestion: (m) => `${m[1]} days`,
|
|
168
|
+
reason: 'Use the plural form "days" after numbers greater than one.',
|
|
169
|
+
}),
|
|
170
|
+
createRegexRule({
|
|
171
|
+
id: 'QA_WORD_two_hour',
|
|
172
|
+
category: 'grammar',
|
|
173
|
+
pattern: /\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million)\s+hour\b/i,
|
|
174
|
+
suggestion: (m) => `${m[1]} hours`,
|
|
175
|
+
reason: 'Use the plural form "hours" after numbers greater than one.',
|
|
176
|
+
}),
|
|
177
|
+
|
|
178
|
+
// ─── HYPHENATED AGE COMPOUNDS ───
|
|
179
|
+
createRegexRule({
|
|
180
|
+
id: 'QA_AGE_month_old',
|
|
181
|
+
category: 'grammar',
|
|
182
|
+
pattern: /\b(\d+)\s+month[\s-]old\b/i,
|
|
183
|
+
suggestion: (m) => `${m[1]}-month-old`,
|
|
184
|
+
reason: 'Hyphenate compound age modifiers: "6-month-old".',
|
|
185
|
+
}),
|
|
186
|
+
createRegexRule({
|
|
187
|
+
id: 'QA_AGE_week_old',
|
|
188
|
+
category: 'grammar',
|
|
189
|
+
pattern: /\b(\d+)\s+week[\s-]old\b/i,
|
|
190
|
+
suggestion: (m) => `${m[1]}-week-old`,
|
|
191
|
+
reason: 'Hyphenate compound age modifiers: "3-week-old".',
|
|
192
|
+
}),
|
|
193
|
+
createRegexRule({
|
|
194
|
+
id: 'QA_AGE_day_old',
|
|
195
|
+
category: 'grammar',
|
|
196
|
+
pattern: /\b(\d+)\s+day[\s-]old\b/i,
|
|
197
|
+
suggestion: (m) => `${m[1]}-day-old`,
|
|
198
|
+
reason: 'Hyphenate compound age modifiers: "2-day-old".',
|
|
199
|
+
}),
|
|
200
|
+
];
|