@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 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.0",
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",
@@ -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) out.push({ entry, preferred, alternatives })
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({ id: entry.id, found: hit[1], expected: preferred, index: hit.index })
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
  }
@@ -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