@capsiynau/intelligence 0.5.0 → 0.5.1
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/CHANGELOG.md +40 -0
- package/package.json +1 -1
- package/src/dialect/index.js +68 -9
- package/types/dialect/index.d.ts +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ All notable changes to `@capsiynau/intelligence` go here. Format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.1] - 2026-07-25
|
|
10
|
+
|
|
11
|
+
**Welsh review of the 0.5.0 seed lexicon. Four of the twelve entries were
|
|
12
|
+
wrong, two of them badly enough to emit ungrammatical Welsh.** Patch rather
|
|
13
|
+
than minor because no consumer resolves 0.5.x yet (`notes-app` and `postio`
|
|
14
|
+
are both on `^0.4.0`, and caret on a 0.x range permits patch only), so this
|
|
15
|
+
corrects a defect nobody has installed.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **`efo`/`gyda` and `isio`/`moyn` are now detect-only.** Both are genuine
|
|
20
|
+
regional pairs, but substituting them changes the grammar of the words
|
|
21
|
+
around them, and this module is mutation-blind and syntax-blind by design.
|
|
22
|
+
`gyda` takes the aspirate mutation and `efo` takes none, so the bare swap
|
|
23
|
+
gave `gyda cath` where the correct form is `gyda chath`. `moyn` takes a
|
|
24
|
+
linking `yn` and `isio` does not, so the bare swap gave `Dw i moyn`, which
|
|
25
|
+
is not grammatical. `checkDialect` still reports both; `applyDialect` now
|
|
26
|
+
skips them. Emitting broken Welsh is worse than leaving a term alone and
|
|
27
|
+
telling a human.
|
|
28
|
+
- **Removed `pres`/`arian` (money) and `dŵad`/`dod` (to come).** These are not
|
|
29
|
+
regional pairs. `arian` and `dod` are the universal standard forms, used in
|
|
30
|
+
the north as readily as the south; `pres` and `dŵad` are northern
|
|
31
|
+
colloquialisms. Filing them as regional meant telling a northern writer that
|
|
32
|
+
their standard Welsh was southern. Register belongs to the prompt fragment,
|
|
33
|
+
not this table. The reason is recorded in `EXCLUDED_TERMS` so the next person
|
|
34
|
+
does not re-add them.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `applySafe` on every `checkDialect` finding and every `listDialectTerms`
|
|
39
|
+
row, plus `applyNote` on detect-only entries. A caller can now tell the
|
|
40
|
+
difference between "this was corrected" and "this needs a human", rather
|
|
41
|
+
than assuming a clean `applyDialect` pass resolved everything.
|
|
42
|
+
|
|
43
|
+
### Note
|
|
44
|
+
|
|
45
|
+
The lexicon has now had a machine-assisted review against house style and
|
|
46
|
+
known Welsh error patterns. That is **not** a native sign-off.
|
|
47
|
+
`DIALECT_TERMS_STATUS.reviewed` stays `false`.
|
|
48
|
+
|
|
9
49
|
## [0.5.0] - 2026-07-25
|
|
10
50
|
|
|
11
51
|
Additive. No existing import path changes shape, so 0.4.x consumers can
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capsiynau/intelligence",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Bilingual (Welsh + English) intelligence layer: text normalisation, mutation handling, digraph splitting, spellcheck primitives, and glossary/correction utilities. Pure functions, runs in Node or browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/dialect/index.js
CHANGED
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
* `welsh/mutations.js`. Callers normalise first or accept the miss.
|
|
28
28
|
* - Touch pronouns or copula forms. See EXCLUDED_TERMS below; the false
|
|
29
29
|
* positives are worse than the misses.
|
|
30
|
+
* - Rewrite a term whose swap would change the grammar around it. Those
|
|
31
|
+
* entries carry `applySafe: false`: checkDialect REPORTS them, applyDialect
|
|
32
|
+
* SKIPS them. "efo" -> "gyda" is the clear case, since gyda takes the
|
|
33
|
+
* aspirate mutation and efo takes none, so the bare swap is ungrammatical.
|
|
34
|
+
* - Carry colloquial-versus-standard pairs. "arian" and "dod" are standard
|
|
35
|
+
* everywhere; "pres" and "dŵad" are northern colloquialisms, not southern
|
|
36
|
+
* opposites. That is register, and register belongs to the prompt fragment.
|
|
30
37
|
* - Infer a region from text. The caller declares it, per the house rule
|
|
31
38
|
* that source language and variety are never auto-detected.
|
|
32
39
|
*
|
|
@@ -62,18 +69,38 @@ const WELSH_LETTERS = "A-Za-zÂÊÎÔÛŴŶâêîôûŵŷÁÉÍÓÚáéíóúÀ
|
|
|
62
69
|
*/
|
|
63
70
|
export const DIALECT_TERMS = [
|
|
64
71
|
{ id: 'now', north: 'rŵan', south: 'nawr', neutral: 'nawr', gloss: 'now' },
|
|
65
|
-
{ id: 'with', north: 'efo', south: 'gyda', neutral: 'gyda', gloss: 'with' },
|
|
66
72
|
{ id: 'milk', north: 'llefrith', south: 'llaeth', neutral: 'llaeth', gloss: 'milk' },
|
|
67
|
-
{ id: 'want', north: 'isio', south: 'moyn', neutral: 'eisiau', gloss: 'to want' },
|
|
68
73
|
{ id: 'out', north: 'allan', south: 'mas', neutral: 'allan', gloss: 'out' },
|
|
69
|
-
{ id: 'money', north: 'pres', south: 'arian', neutral: 'arian', gloss: 'money' },
|
|
70
|
-
{ id: 'to-come', north: 'dŵad', south: 'dod', neutral: 'dod', gloss: 'to come' },
|
|
71
74
|
{ id: 'boy', north: 'hogyn', south: 'crwt', neutral: 'bachgen', gloss: 'boy' },
|
|
72
75
|
{ id: 'girl', north: 'hogan', south: 'croten', neutral: 'merch', gloss: 'girl' },
|
|
73
76
|
{ id: 'cuppa', north: 'panad', south: 'dishgled', neutral: 'paned', gloss: 'a cuppa' },
|
|
77
|
+
|
|
74
78
|
// Both forms regionally marked, no neutral standard.
|
|
75
79
|
{ id: 'grandfather', north: 'taid', south: 'tad-cu', neutral: null, gloss: 'grandfather' },
|
|
76
80
|
{ id: 'grandmother', north: 'nain', south: 'mam-gu', neutral: null, gloss: 'grandmother' },
|
|
81
|
+
|
|
82
|
+
// ── Detect-only: real regional pairs, but SUBSTITUTING them changes the
|
|
83
|
+
// grammar of the surrounding words, and this module is mutation-blind and
|
|
84
|
+
// syntax-blind by design. checkDialect reports them (always safe, and it is
|
|
85
|
+
// exactly what a reviewer wants to see); applyDialect refuses to rewrite
|
|
86
|
+
// them, because a swap that produces ungrammatical Welsh is worse than no
|
|
87
|
+
// swap at all.
|
|
88
|
+
{
|
|
89
|
+
id: 'with', north: 'efo', south: 'gyda', neutral: 'gyda', gloss: 'with',
|
|
90
|
+
applySafe: false,
|
|
91
|
+
applyNote:
|
|
92
|
+
'"gyda" triggers the aspirate mutation and "efo" triggers none, so a ' +
|
|
93
|
+
'bare swap yields "gyda cath" where the correct form is "gyda chath". ' +
|
|
94
|
+
'Fixing it needs the mutation machinery this module does not have.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'want', north: 'isio', south: 'moyn', neutral: 'eisiau', gloss: 'to want',
|
|
98
|
+
applySafe: false,
|
|
99
|
+
applyNote:
|
|
100
|
+
'The three forms take different syntax: "Dw i isio" and "Dw i eisiau" ' +
|
|
101
|
+
'take no linking "yn", but "moyn" does ("Dw i\'n moyn"). A bare swap ' +
|
|
102
|
+
'yields "Dw i moyn", which is not grammatical.',
|
|
103
|
+
},
|
|
77
104
|
]
|
|
78
105
|
|
|
79
106
|
/**
|
|
@@ -82,6 +109,17 @@ export const DIALECT_TERMS = [
|
|
|
82
109
|
* they are absent is not self-evident.
|
|
83
110
|
*/
|
|
84
111
|
export const EXCLUDED_TERMS = [
|
|
112
|
+
{
|
|
113
|
+
id: 'colloquial-not-regional',
|
|
114
|
+
reason:
|
|
115
|
+
'Pairs like "pres"/"arian" (money) and "dŵad"/"dod" (to come) look ' +
|
|
116
|
+
'regional but are not: "arian" and "dod" are the universal standard ' +
|
|
117
|
+
'forms, used in the north as readily as the south, and "pres"/"dŵad" ' +
|
|
118
|
+
'are northern COLLOQUIAL forms. That is a register distinction, and ' +
|
|
119
|
+
'register is the prompt fragment\'s job, not this table\'s. Filing them ' +
|
|
120
|
+
'here would have told a northern writer their standard Welsh was ' +
|
|
121
|
+
'southern. Removed 25.07.2026 after review.',
|
|
122
|
+
},
|
|
85
123
|
{
|
|
86
124
|
id: 'pronoun-o-e',
|
|
87
125
|
reason:
|
|
@@ -171,7 +209,9 @@ function activeEntries(region) {
|
|
|
171
209
|
seen.add(key)
|
|
172
210
|
alternatives.push(form)
|
|
173
211
|
}
|
|
174
|
-
if (alternatives.length)
|
|
212
|
+
if (alternatives.length) {
|
|
213
|
+
out.push({ entry, preferred, alternatives, applySafe: entry.applySafe !== false })
|
|
214
|
+
}
|
|
175
215
|
}
|
|
176
216
|
return out
|
|
177
217
|
}
|
|
@@ -184,11 +224,15 @@ function activeEntries(region) {
|
|
|
184
224
|
* @returns {Array<{id:string, gloss:string, preferred:string, alternatives:string[]}>}
|
|
185
225
|
*/
|
|
186
226
|
export function listDialectTerms(region) {
|
|
187
|
-
return activeEntries(region).map(({ entry, preferred, alternatives }) => ({
|
|
227
|
+
return activeEntries(region).map(({ entry, preferred, alternatives, applySafe }) => ({
|
|
188
228
|
id: entry.id,
|
|
189
229
|
gloss: entry.gloss,
|
|
190
230
|
preferred,
|
|
191
231
|
alternatives,
|
|
232
|
+
applySafe,
|
|
233
|
+
// Present only on detect-only entries, so a UI can explain to the user
|
|
234
|
+
// why this one is flagged but never auto-corrected.
|
|
235
|
+
...(entry.applyNote ? { applyNote: entry.applyNote } : {}),
|
|
192
236
|
}))
|
|
193
237
|
}
|
|
194
238
|
|
|
@@ -205,12 +249,20 @@ export function listDialectTerms(region) {
|
|
|
205
249
|
export function checkDialect(text, region) {
|
|
206
250
|
if (!text) return []
|
|
207
251
|
const findings = []
|
|
208
|
-
for (const { entry, preferred, alternatives } of activeEntries(region)) {
|
|
252
|
+
for (const { entry, preferred, alternatives, applySafe } of activeEntries(region)) {
|
|
209
253
|
for (const alternative of alternatives) {
|
|
210
254
|
const matcher = matcherFor(alternative)
|
|
211
255
|
let hit
|
|
212
256
|
while ((hit = matcher.exec(text)) !== null) {
|
|
213
|
-
findings.push({
|
|
257
|
+
findings.push({
|
|
258
|
+
id: entry.id,
|
|
259
|
+
found: hit[1],
|
|
260
|
+
expected: preferred,
|
|
261
|
+
index: hit.index,
|
|
262
|
+
// false means applyDialect will NOT fix this one: the caller must
|
|
263
|
+
// hand it to a human rather than assume a rewrite resolved it.
|
|
264
|
+
applySafe,
|
|
265
|
+
})
|
|
214
266
|
}
|
|
215
267
|
}
|
|
216
268
|
}
|
|
@@ -224,6 +276,12 @@ export function checkDialect(text, region) {
|
|
|
224
276
|
* Substitution is whole-term only and mutation-blind, so it is safe to run on
|
|
225
277
|
* mixed Welsh/English text: no entry in the table is an English word.
|
|
226
278
|
*
|
|
279
|
+
* Entries marked `applySafe: false` are SKIPPED here and reported by
|
|
280
|
+
* `checkDialect` instead. Those are pairs whose substitution would change the
|
|
281
|
+
* grammar of neighbouring words (mutation or linking "yn"), which this module
|
|
282
|
+
* has no machinery to get right. Emitting ungrammatical Welsh is worse than
|
|
283
|
+
* leaving the term alone and telling a human about it.
|
|
284
|
+
*
|
|
227
285
|
* @param {string} text
|
|
228
286
|
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
229
287
|
* @returns {string}
|
|
@@ -231,7 +289,8 @@ export function checkDialect(text, region) {
|
|
|
231
289
|
export function applyDialect(text, region) {
|
|
232
290
|
if (!text) return ''
|
|
233
291
|
let out = text
|
|
234
|
-
for (const { preferred, alternatives } of activeEntries(region)) {
|
|
292
|
+
for (const { preferred, alternatives, applySafe } of activeEntries(region)) {
|
|
293
|
+
if (!applySafe) continue
|
|
235
294
|
for (const alternative of alternatives) {
|
|
236
295
|
out = out.replace(matcherFor(alternative), (m) => matchCase(m, preferred))
|
|
237
296
|
}
|
package/types/dialect/index.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export function checkDialect(text: string, region: "north" | "south" | "neutral"
|
|
|
34
34
|
* Substitution is whole-term only and mutation-blind, so it is safe to run on
|
|
35
35
|
* mixed Welsh/English text: no entry in the table is an English word.
|
|
36
36
|
*
|
|
37
|
+
* Entries marked `applySafe: false` are SKIPPED here and reported by
|
|
38
|
+
* `checkDialect` instead. Those are pairs whose substitution would change the
|
|
39
|
+
* grammar of neighbouring words (mutation or linking "yn"), which this module
|
|
40
|
+
* has no machinery to get right. Emitting ungrammatical Welsh is worse than
|
|
41
|
+
* leaving the term alone and telling a human about it.
|
|
42
|
+
*
|
|
37
43
|
* @param {string} text
|
|
38
44
|
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
39
45
|
* @returns {string}
|
|
@@ -51,13 +57,23 @@ export namespace DIALECT_TERMS_STATUS {
|
|
|
51
57
|
* standard, so a neutral target leaves the text alone rather than inventing
|
|
52
58
|
* one. Saying "no neutral form exists" is more useful than picking a side.
|
|
53
59
|
*/
|
|
54
|
-
export const DIALECT_TERMS: {
|
|
60
|
+
export const DIALECT_TERMS: ({
|
|
55
61
|
id: string;
|
|
56
62
|
north: string;
|
|
57
63
|
south: string;
|
|
58
64
|
neutral: string;
|
|
59
65
|
gloss: string;
|
|
60
|
-
|
|
66
|
+
applySafe?: undefined;
|
|
67
|
+
applyNote?: undefined;
|
|
68
|
+
} | {
|
|
69
|
+
id: string;
|
|
70
|
+
north: string;
|
|
71
|
+
south: string;
|
|
72
|
+
neutral: string;
|
|
73
|
+
gloss: string;
|
|
74
|
+
applySafe: boolean;
|
|
75
|
+
applyNote: string;
|
|
76
|
+
})[];
|
|
61
77
|
/**
|
|
62
78
|
* Deliberately NOT in the table, with the reason. Kept in code because the
|
|
63
79
|
* next person to extend this will reach for exactly these, and the reason
|